diff --git a/.gitignore b/.gitignore index a4331fb58..cf9896f42 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,12 @@ TextMesh Pro Library *.csproj Runtime/Mapbox/VectorModule/MeshGeneration/.idea + +# Internal benchmark tool, kept local-only +Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs +Runtime/Mapbox/Example/Scripts/TileProviderBenchmark.cs.meta + +# Claude Code local config, kept local-only +CLAUDE.md +CLAUDE.md.meta +.claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e76e625c..525a53a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,256 @@ ## CHANGELOG -### v3.0.7 +### v3.1.1 -#### Changes -- Added new documentation. -- Added a new demo scene demonstrating how to use POI information. -- Introduced new inspector scripts to improve editor UI/UX. -- Reorganized and cleaned up the Mapbox context menu used for creating Mapbox ScriptableObjects. +Patch release. Reclassifies `com.unity.inputsystem` from a soft to a **hard** package dependency and switches the example-assembly `#if` gate from package presence to Player Settings, restoring compilation on Unity 6+ projects where the input system package ships preinstalled. Existing legacy-input projects updating from 3.1.0 see no runtime behavior change. + +#### Why this exists + +In 3.1.0 the SDK declared `com.unity.inputsystem` as a soft dependency: source code branched on a `MAPBOX_NEW_INPUT_SYSTEM` define wired via `versionDefines` (set when the package is installed), but `MapboxExamples.asmdef` did not actually reference `Unity.InputSystem`. On Unity 6+ — where the input system package is installed by default — the `#if MAPBOX_NEW_INPUT_SYSTEM` branch activated, the `using UnityEngine.InputSystem` failed to resolve at the asmdef level, and the entire Examples assembly failed to compile. + +The mirror failure mode (asmdef referencing `Unity.InputSystem` without the package installed → unresolved-reference compile error in 2022.3 legacy projects) ruled out simply restoring the reference. Source-side `#if` cannot bridge this because asmdef reference resolution happens before C# preprocessing. + +This release resolves the trade-off by treating the package as a hard dependency that's always installed, and gating the source-side branch on a Unity built-in define that's only set when the user explicitly opts in via Player Settings. + +#### Changed + +- `com.unity.inputsystem` (>= 1.7.0) is declared in `package.json` `dependencies`. UPM auto-installs it on next package refresh. The package coexists with legacy input; installation alone does not change runtime behavior. +- `PointerInput.cs` now gates on Unity's built-in `ENABLE_INPUT_SYSTEM` define (set by Player → Active Input Handling = "New" or "Both") instead of `MAPBOX_NEW_INPUT_SYSTEM`. Existing legacy-input projects keep running the `#else` branch until the user explicitly switches Active Input Handling. +- `MapboxExamples.asmdef` references `Unity.InputSystem` directly and drops the `MAPBOX_NEW_INPUT_SYSTEM` `versionDefines` block (no longer used). + +#### Upgrading an existing project + +For the common case — Unity 2022.3 project, Active Input Handling = "Old", no prior `com.unity.inputsystem` install — no action is required. After the SDK update, UPM installs the input system package (~2 MB), Active Input Handling stays "Old", and every code path runs the same legacy implementation it did before. + +If the upgrade goes wrong, in roughly decreasing order of likelihood: + +- **Stale compile errors that mention `UnityEngine.InputSystem` or `MAPBOX_NEW_INPUT_SYSTEM`** — Unity's `Library/` cache hasn't picked up the new package. Close Unity, delete `Library/`, reopen. Unity re-resolves packages and reimports. +- **Package Manager reports "cannot resolve com.unity.inputsystem"** — the project is on Unity < 2022.3, which the SDK does not support, or a project-side `Packages/manifest.json` override pins an incompatible version. Bump the project's Unity version or relax the version constraint. +- **Map cameras stop responding to mouse / touch after the update** — should not happen if Active Input Handling matches what it was before; if it does, verify Project Settings → Player → Active Input Handling is still set to your prior choice. Unity does not change this setting when a package is auto-installed, but a manual setting flip during the update would explain the symptom. +- **Yellow Inspector warning on `StandaloneInputModule` asking to "Replace with InputSystemUIInputModule"** — cosmetic, appears only under Active Input Handling = "New" / "Both". One-click swap on the EventSystem inspector if you want to clear it; runtime is unaffected. +- **UGUI throws `InvalidOperationException: You are trying to read Input using the UnityEngine.Input class…` under Active Input Handling = "New" only** — Unity's default `StandaloneInputModule` reads through `UnityEngine.Input.*`, which throws under New-only. Either set Active Input Handling to "Both" (legacy stays available for UGUI), or click "Replace with InputSystemUIInputModule" on the EventSystem inspector. +- **You want to remove `com.unity.inputsystem` entirely from your project** — UPM blocks removal because the SDK declares it as a dependency. The de facto opt-out is to leave it installed and set Active Input Handling = "Old"; the package is then dead weight on disk but runtime is identical to legacy-only. For a true physical removal, edit the SDK's `package.json` to drop the `com.unity.inputsystem` line and remove the matching GUID reference from `MapboxExamples.asmdef`. Requires the SDK to be in an editable location (e.g. embedded under `Packages/`, not in `Library/PackageCache/`). + +### v3.1.0 + +Minor-version bump per semver: this release contains source-breaking API changes (listed below) alongside substantial feature work in camera/input, tile LOD, and terrain rendering. The 3.0.7 designation was previously used in development; published 3.0.7 builds, if any, are superseded by 3.1.0. + +#### New dependencies +- Added `com.unity.burst@1.8.12`. The terrain-RGB elevation decoder and the collider vertex-fill jobs are now Burst-compiled. First-time domain reload pays a one-shot AOT compile cost; runtime decoding is significantly faster. +- `com.unity.inputsystem` is recognized as a **soft dependency**. Projects without it continue using `UnityEngine.Input`; projects with it automatically use the new Input System via a `MAPBOX_NEW_INPUT_SYSTEM` define wired through `versionDefines`. No project-side action required either way. See `Documentation~/CameraSystem.md` for details. + +#### Breaking changes + +**Source-level API surface:** + +- `IMapInformation` gained a new member: `TerrainInfo Terrain { get; }`. Any project that ships its own `IMapInformation` implementation must add this property — return an instance of `TerrainInfo` (defaults are fine for a non-terrain map). The built-in `MapInformation` already implements it. +- `TerrainData.ElevationValuesUpdated` is now declared `public event Action` (was a plain `public Action` field). External code can subscribe with `+=` / unsubscribe with `-=` as before, but **direct assignment (`= myCallback`) and direct invocation (`ElevationValuesUpdated()`) no longer compile**. Migrate to `+=` for subscription. The previous single-setter `SetElevationChangedCallback` (which silently wiped other subscribers) has been removed. +- `MapboxTileData.SetDisposeCallback` was replaced with `public AddDisposeCallback(Action)` / `RemoveDisposeCallback(Action)` (multicast). External code that previously assigned through reflection or an internal-visible-to dependency should switch to the add/remove pair; subscriptions are now safely shared across the up-to-16 render tiles that consume the same `TerrainData` instance. +- `TerrainData.IsDisposed` is a new `public bool` (read-only). Async readback paths check this before assigning into the data; external consumers may also want to consult it. +- `MapboxMapVisualizer` now implements two new interfaces: + - `IMapVisualizer` (existing, no change). + - `ITileLifecycleSource` (new, in `Mapbox.BaseModule.Map`): exposes `TileLoaded` / `TileUnloading` events, `MapInformation` property, and `ActiveTiles` as `IReadOnlyDictionary` via explicit interface implementation. The public concrete `ActiveTiles` property still returns `Dictionary<,>` — no break for existing consumers. +- New opt-in marker interface `ITileLifecycleObserver` (in `Mapbox.BaseModule.Map`). Layer modules that need a visualizer reference for bookkeeping can implement it and the visualizer calls `AttachToMapVisualizer(ITileLifecycleSource source)` on them during `Initialize`. `TerrainLayerModule` implements it for its terrain-bounds tracker; other modules are unaffected. + +**Custom imagery module:** + +- `CustomTMSTile` constructor extended from 4 args → 6 args: added `bool invertY` and `bool isMapboxService`. The legacy 4-arg constructor (`urlFormat`, `tileId`, `tilesetId`, `useNonReadableTexture`) is preserved as an overload defaulting to `invertY: true, isMapboxService: false` — external subclasses keep compiling. New code should use the 6-arg form to control invert-Y behaviour and Mapbox-signed request routing explicitly. +- `CustomSource.CreateTile` now always returns a `CustomTMSTile` when `UrlFormat` is set, regardless of `InvertY`. Previously `InvertY=false` returned a plain `RasterTile`. If you relied on the plain-raster path with a non-empty `UrlFormat`, set `UrlFormat = ""` to keep that behavior — empty `UrlFormat` falls back to plain `RasterTile`. +- `CustomTerrainSource.CreateTile` got the same empty-`UrlFormat` fallback to plain `RasterTile` to match `CustomSource`. +- `CustomTerrainLayerModuleScript` now passes the user-configured `Settings.DataSettings` (cache size, retina flag, non-readable textures, data-zoom clamp) through to `CustomTerrainSource`. Previously a throwaway `ImageSourceSettings` was used and the Inspector values had no runtime effect. **This is a behavioral break for users who had `DataSettings` set to non-default values** — the values now actually apply. + +**Terrain settings:** + +- `TerrainInfo` removed two fields that were written-but-never-read: `IsEnabled` (was unused) and `Exaggeration` (no consumer). Remaining fields: `MinElevation`, `MaxElevation`. Plus new constants `DefaultMinElevation = 0f` and `DefaultMaxElevation = 5000f`. +- The previous `SetElevationChangedCallback` method on `TerrainData` was removed (replaced by the multicast event — see above). + +**Camera API:** + +- New `MapCameraBehaviour` open generic abstract base class. Concrete subclasses (`SlippyMapCameraBehaviour`, `Moving3dCameraBehaviour`) ship with the SDK. Custom cameras must subclass with a closed-type generic parameter. +- New `MapInput.Teardown(IMapInformation)` virtual hook. Existing subclasses of `MapInput` must override and unsubscribe any events they registered in `Initialize` — without the override the default is a no-op (no break, but recommended). +- New `MapInput.TouchCountDecreasedThisFrame` protected property (read inside `UpdateCamera` to detect 2→1 finger pinch-end and re-seed drag origin). +- New camera state mutator: `MapInput.CurrentTwoFingerGesture` removed from earlier review iterations — moved entirely inside `TouchInputHandler` as a private enum. External code can't depend on it (was never publicly exposed in v3.0.x). + +**Tile provider:** + +- `UnityTileProvider.MaximumZoomLevel` is now silently clamped to **30** internally (was unbounded). Higher values would have overflowed `1 << (_maxZoom - zoom)` past 30. Practically unreachable in Mercator (pyramid maxes at 22-24), but any Inspector misconfig now produces a log warning instead of an overflow. +- `UnityTileProvider` now warns at construction if `MinimumZoomLevel > MaximumZoomLevel`. +- `TileNode.Set` internal-shape signature changed from `(id, worldCenter, scale, boundsHeight)` to `(id, worldCenter, scale, boundsBottom, boundsHeight)`. Internal API — only relevant if you subclassed `UnityTileProvider` and touched `TileNode` directly. + +**Map visualizer:** + +- `MapboxMapVisualizer.MaxMercatorZoom` is a new `public const int` (`= 22`). Replaces the magic literal in `DelveInto`. Available for external consumers that need to align with the SDK's pyramid cap. +- `MapboxMapVisualizer.MapInformation` is a new public property exposing the map info. Was previously a protected field only. + +**Removed from package:** + +- `TileProviderBenchmark.cs` was removed from the SDK package. It was an internal tool that shouldn't have been included. The file is `.gitignore`d so a local copy can be kept by developers who need it; users will not see it in the SDK. + +#### New features + +**Camera & input:** + +- New camera system with two concrete behaviours: + - `SlippyMapCameraBehaviour` — fixed camera, map moves underneath. Best for 2D-style maps, navigation overlays, AR/VR. + - `Moving3dCameraBehaviour` — camera orbits a target point. Best for 3D city exploration, games, simulations. + Both wrap a `MapCameraBehaviour` generic base that handles the `IMapInformation` event subscription lifecycle. +- Touch input support: single-finger pan, two-finger pinch zoom, two-finger same-direction vertical tilt. Pinch/tilt are mutually exclusive via a per-frame gesture decision in `UpdateInputState`. Touch rotate is not implemented (mouse-only via right-click drag). +- Input System soft-dependency support. The SDK reads through an `IPointerInput` abstraction (legacy vs new system) selected at compile time by `MAPBOX_NEW_INPUT_SYSTEM`, with two `IInputHandler` implementations (mouse vs touch) selected by build platform — Android/iOS device builds use the touch handler; Editor and Standalone builds use the mouse handler. There is no runtime fallback. +- `Documentation~/CameraSystem.md` walks through the camera setup, gesture mapping, and the Input System opt-in. + +**Tile provider / LOD:** + +- LOD overhaul: + - Forward-projected distance replaces euclidean distance. + - Exponential split threshold replaces screen-fraction. + - Acute-angle compensation (`DistToSplitScale`) reduces detail for tiles viewed at grazing angles, ported from the native SDK's `tile_cover.cpp`. + - Result: tile counts at high pitch are 40–60% lower vs the prior algorithm with comparable visual fidelity near the camera. +- `FrustumBuffer` setting (world-space units; scales with `mapInformation.Scale`). Pre-loads tiles just outside the screen edges so panning doesn't pop in. +- Reusable scratch arrays (`_corners`, `_planes`, `_quadrants`, `_pool`) eliminate per-frame allocations in `GetTileCover` and `InternalUpdateCoroutine`. +- Sub-sea-level terrain handled correctly: `boundsBottom` derived from `MinElevation/scale`, so tiles below Y=0 (Death Valley, Dead Sea) no longer frustum-cull. + +**Terrain rendering:** + +- Burst-compiled decode of terrain-RGB into elevation float[] via `Sync/AsyncExtractElevationArray` Burst `IJob`s. Min/Max computed inline during decode (saves a second C# pass). +- Burst-compiled collider vertex-fill (`BuildColliderVerticesJob`) writing into a reusable `NativeArray`. Triangles regenerated only when `SampleCount` changes. +- Async PhysX collider bake (when `asyncBakeCollider = true`): `Physics.BakeMesh` runs on a worker thread; `Mesh.sharedMesh` assignment happens one frame later when the cooked-data cache is ready. Main-thread cost is near-zero. +- Shader-mode tiles share a single flat render mesh (`TerrainSharedFlat`) across the entire tile pool. Per-tile state (`_HeightTexture_ST`, `_TileScale`, etc.) goes via `MaterialPropertyBlock` instead of per-tile `Material` instances — saves `Material` clone allocations across pool churn. +- Bilinear elevation sampling in `TerrainData.QueryHeightData`, the collider vertex job, and the shader bilinear subgraph. Replaces the prior nearest-neighbor `(int)xx` truncation that produced visible stepping when render-tile mesh vertices sampled a denser sub-region than the data tile's effective resolution. +- `ElevatedTerrainShader.shadergraph` and `MapboxTerrainBilinearSampling.shadersubgraph` were restructured during the bilinear-sampling and normal-banding work (ridge-band fix uses a 2-texel offset, per Option A in the prior investigation). Lighting and displacement look correct on eye-test against the example scenes. If a project ships custom shader variants derived from these graphs, re-verify after upgrading. +- `TerrainInfo` exposes observed `MinElevation` / `MaxElevation` (in meters) on `IMapInformation.Terrain`. Updated automatically as terrain tiles load/unload. The tile-provider's AABB and `UnityMapTile.SetFallbackMeshBounds` consume this to size the frustum-cull volume. +- Conservative bounds fallback (`UnityMapTile.SetFallbackMeshBounds`, 10000m) applied immediately on terrain-data attach so shader-displaced geometry doesn't get culled before real Min/Max arrive — or permanently, when CPU extraction is disabled. +- Per-tile `MeshCollider` configurable via `TerrainColliderOptions` (in `ElevationLayerProperties`). Supports placing the collider on a dedicated child GameObject + Unity Layer. +- Bounds tracking is now an opt-in concern of `TerrainLayerModule` (via `ITileLifecycleObserver` → `TerrainBoundsTracker`). `MapboxMapVisualizer` itself is terrain-agnostic. + +**Custom imagery:** + +- New `CustomSourceSettings.InvertY` toggle: when `true`, Y is `(2^z - Y - 1)` instead of `Y` in URL substitution (TMS-style). +- New `CustomSourceSettings.IsMapboxService` toggle: when `true`, the tile request is signed with the Mapbox access token; when `false`, it's a plain HTTP request. + +**Editor / Inspector:** + +- New custom drawer for `[SimplificationFactor]` attribute: dropdown of distinct-grid presets with a live HelpBox describing the resulting vertex grid + per-tile quad count. Legacy non-preset values now show a "migrate to a preset" warning instead of being silently snapped. Multi-object editing and prefab-override-safe via `BeginProperty`/`EndProperty`. +- New custom drawer for `TerrainColliderOptions`: foldout with grayed-out sub-options when `addCollider` is disabled, further gated when `useDedicatedColliderLayer` is disabled. +- New custom drawer for `TerrainSideWallOptions`: same reveal pattern for skirt-wall settings. +- New custom drawer for `[GameObjectLayer]` attribute: Unity Layer dropdown for `layerId` fields. +- `TerrainSettingsInspectorHelper` warns when extraction is force-enabled by other settings (UseShaderTerrain off, or collider on) and renders the `ExtractCpuElevationData` checkbox as disabled+ticked. The previous OnGUI silent write to `boolValue` was removed — no more silently dirtying inspected assets. Scene-scan for vector / components modules is cached and invalidated on `EditorApplication.hierarchyChanged`. + +**Other:** + +- New demo scene demonstrating how to use POI information. +- New documentation files added. +- Reorganized Mapbox context menu for creating Mapbox ScriptableObjects. + +#### Performance + +- Burst-compiled terrain-RGB decode: significantly faster than the prior C# loop. +- Burst-compiled collider vertex fill with persistent `NativeArray` buffers. +- `ElevationArrayPool`: pools `float[]` buffers across cache evictions, capped at 32 per-size (`MaxPerSizeDepth`). Avoids ~256 KB / ~1 MB allocations per tile decode for 256² / 512² heightmaps. Returns past the cap drop to GC. In Editor, the pool clears on `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` so stale buffers don't survive play-mode stop. +- `DelveInto` rewritten as iterative explicit-stack: eliminates `new bool[4]` + `new UnwrappedTileId[4]` allocations per cache miss (up to ~85 recursive calls × ~32 bytes before). +- `MapboxMapVisualizer.Load` reuses `unityMapTile.Children` lists across pool cycles instead of allocating a fresh list per cache miss. +- `TerrainBoundsTracker` recomputes Min/Max from `ActiveTiles` once per coroutine tick (deferred via a dirty flag) instead of on every tile event. +- `TerrainData._cachedWidth` caches `sqrt(ElevationValues.Length)` once on `SetElevationValues` — `QueryHeightData` no longer recomputes per query. +- `SimplificationFactorDrawer` caches preset+label arrays per `(Min, Max, VertexBase)` tuple and reuses one `GUIContent` for HelpBox height calculation (was allocating per repaint). +- `TerrainSettingsInspectorHelper` scene scan no longer runs `FindObjectsByType` on every OnGUI repaint. #### Fixes -- Fixed an issue where vector layer range limits were not applied correctly during data processing. + +**Tile pipeline:** + +- Fixed multicast dispose notification — shared `TerrainData` now notifies all consumers on eviction (previously the single-setter callback silently dropped 15 of 16 sharing tiles). +- Fixed `TerrainData.ElevationValuesUpdated` previously being a plain `public Action`, which let `SetElevationChangedCallback` silently wipe other subscribers. Now a proper multicast event. +- Fixed `TerrainSource.ExtractElevationValues` using the wrong overload of `ExtractHeightData` — the `Action` overload + 1-arg `SetElevationValues` left `Min/MaxElevation` at 0 on every tile loaded through the initial async path. Now uses the `TerrainData` overload (which sets Min/Max inline). +- Fixed `TerrainSource.ExtractElevationValues` hanging forever when async readback completes after `TerrainData.Dispose` — the coroutine now subscribes to both `ElevationValuesUpdated` and the dispose multicast and completes on either signal. +- Fixed `Async/SyncExtractElevationArray` not guarding against `TerrainData.IsDisposed` — late-arriving GPU readback callbacks would assign into evicted data and leak the rented buffer. Now both check before renting and after the decode; the rented buffer is returned to the pool when dispose arrives in the window. +- Fixed `MapboxMapVisualizer.RecomputeTerrainBounds` (now in `TerrainBoundsTracker`) not being wired to `ElevationValuesUpdated` for shader-mode tiles — those tiles finish on texture-ready before CPU decode arrives, so the bounds never widened until extraction landed. Now a one-shot watcher per shared `TerrainData` triggers the recompute. +- Fixed terrain bounds remaining pinned at 0 when all loaded tiles were at/above sea level (previous accumulate-only `< terrain.MinElevation` never fired). +- Fixed terrain bounds monotonically inflating over a long session — `TerrainBoundsTracker` now recomputes from `ActiveTiles` on tile pool, so bounds tighten as the camera moves to lower-elevation regions. +- Fixed `UnityTileTerrainContainer.SetTerrainData(null)` silently leaking the prior `TerrainData`'s `ElevationValuesUpdated` and dispose-callback subscriptions. Detach now runs unconditionally before the null-return. +- Fixed `UnityTileTerrainContainer.OnDestroy` not removing the dispose callback (only the `ElevationValuesUpdated` handler) — a later eviction would fire the multicast into a destroyed tile. +- Fixed `UnityTileImageContainer.OnDestroy` being empty — same multicast-leak pattern as the terrain container. + +**Terrain strategy / collider:** + +- Fixed CPU-elevation render mesh leak via `MeshFilter.mesh` clone path. `CreateElevatedMesh` now reads `.sharedMesh` (the `.mesh` getter implicitly cloned the assigned shared mesh and orphaned the original). Per-tile mesh is now explicitly named `TerrainCpuMesh` so `UnityMapTile.OnDestroy` correctly disposes it. +- Fixed `_sharedFlatMesh` being `Clear()`'d on a shader→CPU transition when `sampleCount` or `_useTileSkirts` changed at runtime — would wipe geometry for every other shader-mode tile pointing at the shared mesh. Now allocates a fresh per-tile mesh first. +- Fixed `_sharedFlatMesh` leaking on a second `Initialize` call. `Initialize` now destroys the previous one before allocating. +- Fixed `MeshCollider.cookingOptions` only being set at component creation — when an existing collider's cooking options diverged from the bake's, PhysX silently re-cooked synchronously on assignment, defeating the async-bake path. Now assigned every call. +- Fixed `CompleteBakeAndAssign` not null-guarding the new mesh after `JobHandle.Complete()` — a parallel bake completion could `Destroy()` our mesh as its "previous" before we got to assign it. +- Fixed `_elevationNativeMirror` cache key being the `float[]` reference. `ElevationArrayPool` recycles buffers, so the same `float[]` could be re-rented for different data and produce a false cache hit. Now keyed by `TerrainData` reference. +- Fixed `RegisterCollider` deferred path adding a closure for every temp→final re-invocation, doubling async-bake work. Now de-dups by `(tile, data)`. +- Fixed deferred-rebuild closures rooting `tile` and `data` when `TerrainData.Dispose` arrived before `ElevationValuesUpdated` fired. Closures now self-detach on both signals. +- Fixed `_lastColliderBuild` (collider build cache) growing with dead `MeshCollider` keys — Unity's overloaded `==` doesn't reach `Dictionary`'s reference equality. Opportunistic sweep + `Clear()` on `OnDestroy`. +- Fixed mesh-name string duplication: `TerrainColliderMeshName`, `SharedFlatMeshName`, `TerrainCpuMeshName` are now `public const` on `ElevatedTerrainStrategy`. The dedicated-collider-layer GameObject name renamed from `"TerrainCollider"` to `"TerrainColliderChild"` to disambiguate from the mesh name. + +**Tile provider:** + +- Fixed `UnityTileProvider.ShouldSplit` overriding the per-corner `offset.y` to `+cameraHeight` — correct in the native SDK's tile-coordinate system but flipped the sign of the vertical dot-product contribution in Unity's world units, triggering "always split" for downward-pitched cameras. Now uses ground-plane projection (`offset.y = 0f`). +- Fixed `UnityTileProvider.DistToSplitScale` dividing by `dz` with no zero guard — camera-at-ground (`dz=0`) produced NaN/Inf and pinned the tile to `MinimumZoomLevel`. Now short-circuits with `return 1f` when `dz < 0.0001f`. +- Fixed `UnityTileProvider.ShouldSplit` always-split for behind-camera corners recursing all the way to `_maxZoom` on tilted views with `FrustumBuffer` expansion. Now capped at `zoom < 18` (`AlwaysSplitSafeCap`). +- Fixed `UnityTileProvider.FrustumBuffer` tooltip not mentioning that the value is in world-space units that depend on `mapInformation.Scale`. + +**Camera & input:** + +- Fixed `MAPBOX_NEW_INPUT_SYSTEM` define gating being broken — the `versionDefines` entry that defined the symbol was uncommitted. Projects with "Active Input Handling = New" only would crash on every legacy `UnityEngine.Input.*` call. +- Fixed `MapboxExamples.asmdef` requiring `Unity.InputSystem` as a hard reference, breaking projects without the package. Now relies on the InputSystem asmdef's `autoReferenced: true` resolution; absent → no reference, no compile error. +- Fixed scroll-zoom delta magnitude differing ~10× between legacy and new input paths on Windows (legacy `Input.GetAxis` ≈ 0.1/notch, new `scroll.y` ≈ 120/notch). Normalized to ~0.1/notch on both. +- Fixed pan jump after a 2-finger pinch drops to 1 finger — the new "primary" touch could be at a different screen position than the lifted one. Now re-seeds drag origin on `TouchCountDecreasedThisFrame`. +- Fixed pinch and tilt detectors both firing on the same frame (each computed dominance independently and `GetPinchZoomDelta` updated `_previousPinchDistance` regardless of which won). Decision now made once per frame in `UpdateInputState`. +- Fixed pinch/tilt thresholds being in pixel units (DPI-dependent). Both magnitudes now normalized to `Screen.height`; pinch wins ties (no dropped frames on near-vertical pure-tilt drags). +- Fixed pinch state corruption across 2→3→2 finger transitions. EnhancedTouch reorders `activeTouches` on add/remove, so the next frame would compare distance against a different physical pair. Now tracks the touch ID pair and reseeds on change. +- Fixed `Moving3dCamera.Zoom()` NaN propagation through `preDistance`, `camDistanceToMouse`, and the lerp output. Top-down ortho / first-frame / `mapInformation.Scale=0` / camera curve evaluating to 0 all hit the guards now. +- Fixed `MapCameraBehaviour.Awake` NRE when no `MapBehaviourCore` exists in the scene. Logs an error and disables the component instead. +- Fixed `MapCameraBehaviour` never unsubscribing from `MapBehaviour.Initialized` — leak across scene reload. +- Fixed `MapCameraBehaviour` not tearing down `IMapInformation` event subscriptions registered by the camera core (`LatitudeLongitudeChanged`, `ViewChanged`, `SetView`) — the closures rooted the destroyed camera and its `Camera` Transform. New `MapInput.Teardown` virtual hook + `OnDestroy` wiring. +- Fixed `MapCameraBehaviour.OnMapInitialized` not tearing down a previous `MapInformation` if the event fires twice (defensive — not currently exercised by example scenes). +- Fixed `SlippyMapCamera.Initialize` dereferencing `FindObjectOfType().transform` without a null check. +- Fixed `SlippyMapCamera.CenterLatitudeLongitude` being overwritten every frame even when nothing moved, with a `Mathf.Clamp((float)Latitude, …)` downcast losing precision at z18+. Now gated on `_output.HasChanged` and uses `System.Math.Clamp` on doubles. + +**Map visualizer:** + +- Fixed `MapboxMapVisualizer.RemoveUnnecessaryTiles` previously exempting `Filler` tiles from removal — caused orphaned-filler z-fighting. Now eligible for pooling; visual continuity preserved by the temp-tile filler protection pass in `Load()`. +- Fixed `MapboxMapVisualizer.OnDestroy` not detaching pending elevation-decode watchers — closures kept shared `TerrainData` rooted past the visualizer's lifetime. Tracker (now in `TerrainBoundsTracker`) handles this in its `Dispose`. +- Fixed `LoadingState.Filler` children of currently-loading temp tiles being pooled when they're still needed for visual continuity. Filler protection pass in `Load()` removes their ids from `_toRemove`. + +**Custom imagery:** + +- Fixed `CustomSource.CreateTile` throwing `ArgumentNullException` from `string.Format(null, …)` when `UrlFormat` was empty. Empty `UrlFormat` now falls back to plain `RasterTile`. +- Fixed `CustomTerrainSource.CreateTile` missing the same empty-`UrlFormat` fallback. +- Fixed `CustomTerrainLayerModuleScript` not passing the user's `DataSettings` through to the source — Inspector values had no runtime effect. +- Restored 4-arg `CustomTMSTile` constructor as a backward-compat overload (binary break for external subclasses otherwise). + +**Editor / Inspector:** + +- Fixed `SimplificationFactorDrawer` silently snapping legacy non-preset values to the nearest preset during `OnGUI` — dirtied every inspected asset on first paint. +- Fixed `SimplificationFactorDrawer` missing `BeginProperty`/`EndProperty` — broke prefab-override and multi-object editing. +- Fixed `SimplificationFactorDrawer.GetPropertyHeight` returning a different height than the rendered HelpBox content for legacy values — caused HelpBox to clip into the next Inspector row. +- Fixed `TerrainColliderOptionsDrawer.GetPropertyHeight` returning one extra `line + spacing` (cosmetic gap at the bottom of the foldout). +- Fixed `TerrainSettingsInspectorHelper` writing `child.boolValue = true` during `OnGUI` — silently dirtied every inspected asset. Now display-only (runtime evaluates `NeedsCpuElevation` directly). +- Fixed `TerrainSettingsInspectorHelper.SceneHasFeaturesNeedingCpuElevation` running `FindObjectsByType` on every Inspector repaint. Cached and invalidated on `EditorApplication.hierarchyChanged`. +- Fixed `Runtime/Mapbox/VectorModule/ComponentSystem/Editor/` missing an asmdef — editor scripts (`BuildingLayerVisualizerObjectEditor`, `ExtrusionOptionsDrawer`, `MapboxComponentsModuleScriptEditor`) were being folded into the runtime `MapboxComponentSystem` assembly, pulling `UnityEditor` references into player builds. New `MapboxComponentSystem.Editor.asmdef` (Editor-only, references the runtime asm + `MapboxBaseModule`) keeps them out. +- Fixed `MapboxMapBehaviour.CreateMapVisualizer` silently producing a magenta tile material in player builds when no `TileCreatorBehaviour` was assigned. The previous fallback used `Shader.Find` to construct a Material at runtime, but `Shader.Find` only resolves shaders referenced from a shipped Material or listed in *Project Settings → Graphics → Always Included Shaders* — so the shader was stripped from builds and `new Material(null)` produced magenta tiles. Replaced with a new `[SerializeField] protected Material _defaultTileMaterial` on `MapboxMapBehaviour`; the asset reference keeps the shader alive in builds. Demo scenes (`LocationExample.unity`, `ApiTest.unity`) ship with this field wired to `ElevatedTerrainMaterial.mat`. If both `_tileCreatorBehaviour` and `_defaultTileMaterial` are null, an `InvalidOperationException` is thrown with actionable instructions. +- Fixed `UnityTileProviderSettings.SubdivisionBias` defaulting to `0` in scenes saved before the field existed — Unity assigns `default(float)` for missing serialized fields and does NOT run the C# field initializer in that case, which collapsed `distToSplit` and forced the tile provider to render only the `MinimumZoomLevel` tile. New C# default is `0.6f`; `ISerializationCallbackReceiver.OnAfterDeserialize` upgrades legacy serialized `<= 0` values to `0.6f`. + +**Other:** + +- Fixed `TerrainData.QueryHeightData(Vector2)` and `(float, float)` overloads not guarding against null `ElevationValues` — shader-only mode (`ExtractCpuElevationData=false`) hit the NRE. +- Fixed `TerrainData.Dispose` not being idempotent — explicit `IsDisposed` bail at top. +- Fixed `MapboxTileData.SetDisposeCallback` previously silently overwriting other subscribers when one `TerrainData` was shared across 16 render tiles (the single-callback API). Replaced with multicast `AddDisposeCallback` / `RemoveDisposeCallback`. +- Fixed `1 << (_maxZoom - zoom)` shift overflow risk in `UnityTileProvider.ShouldSplit` when `_maxZoom` exceeded 30. Now clamped internally. +- Fixed vector layer range limits not being applied correctly during data processing. - Fixed incorrect limit validation and bounds checking in the `LatitudeLongitude` struct. +- Fixed `MapInformation.Initialize` not resetting `Terrain.Min/MaxElevation` to defaults — previous run's values could bleed into a fresh session. +- Fixed Android IL2CPP player builds with **Medium** Managed Stripping Level silently dropping every SQLite tile cache insert with a `ConstraintForeignKey` error. Sqlite-net populates row values via reflection (`PropertyInfo.SetValue`), but IL2CPP stripped the auto-property accessor methods (`set_id`, `set_name`, etc.) on the cache's model classes — `SetValue` then became a silent no-op, every read returned `id = 0`, and downstream `tiles` inserts failed the foreign key on `tile_set`. The four SQLite model classes (`tiles`, `tilesets`, `offlineMaps`, `tile2offline`) and every one of their persisted properties now carry `[UnityEngine.Scripting.Preserve]`, and the same types are declared in `Plugins/link.xml` as a secondary safety net. Map rendering was unaffected (memory cache covered it) but the SQLite disk cache wasn't actually persisting under Medium. **Stripping support after the fix:** Low (and below) is the tested target; Medium has been briefly verified and seems to work; High is untested but expected to work given the same preservation. Additionally, the `Error inserting …` log line now includes SQLite's extended error code (`ConstraintUnique` / `ConstraintForeignKey` / `ConstraintNotNull` / etc.) so future cache-side failures are diagnosable from the first line. +- Fixed `MapboxMapVisualizer` writing `tile.transform.position` (world space) on every tile cover update, which silently reset any parent transformation on the `MapRoot` / `BaseTileRoot` hierarchy. Tiles are now positioned via `transform.localPosition`, so translating or rotating the map's root (e.g. anchoring it to an `ARAnchor`) is honored. For unchanged scenes — `MapRoot` at world origin with identity rotation — behavior is identical. Non-unit `MapRoot.localScale` is still unsupported; drive map scale through `MapInformation.Scale` instead. +- Fixed vector content (buildings, areas, roads from `VectorLayerVisualizer` and the ComponentSystem `AreaComponentVisualizer` / `RoadComponentVisualizer`) failing to follow the same MapRoot parent transform as the terrain tiles. Two distinct issues compounded each other. (1) Four call sites were writing world-space positions on the vector hierarchy: the per-layer `GameObjectOffset` y-offset in both component visualizers, the `Settings.Offset` application + `_layerRootObject.transform.position.x/z` reads inside `VectorLayerVisualizer.UpdateForView`, and `MapShifterCore.Update` writing `RuntimeGenerationRoot.position -= viewCenterPosition`. All converted to `localPosition`. (2) `Transform.SetParent(parent)` defaults to `worldPositionStays: true`, which makes Unity preserve world transform by writing inverse-parent-rotation into the child's `localRotation`. Auto-created roots (`BaseTileRoot`, `RuntimeGenerationRoot` in `UnityContext`, and every per-layer `_layerRootObject` + pooled entity GameObject in the vector visualizers) hit this path on initial parenting, ending up with a `localRotation` that counter-rotated the entire vector subtree against MapRoot. All five `SetParent` call sites now pass `worldPositionStays: false`, with explicit `localRotation = Quaternion.identity` on the visualizer roots as defensive belt-and-suspenders. Buildings, areas, and roads now rotate / translate with the rest of the map under AR/MR-style parent transforms. + +#### Documentation +- New [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) walks through every source-level break, every behavioral change, and step-by-step verification when upgrading from v3.0. +- `README.md` rewritten and now serves as the single landing page (Overview.md merged in and removed). Adds a quick-facts header table, dependency listing, demo-scenes table, iOS / Android build sections with the v3.1.0 stripping notes, modules table, and a complete documentation index. +- `Documentation~/GettingStartedWithMapboxMapObject.md` gained a "Place a GameObject at a latitude / longitude" recipe — the most commonly asked indie use case (drop a marker / character / POI at specific coordinates), with the correct `localPosition` + `MapRoot` parenting pattern and the optional `TryGetElevation` step. + +#### Tests +- Added editor-mode tests for `ElevationArrayPool` (rent / return / pool cap / null safety), `TerrainInfo` (defaults), and `MapInformation` (Initialize resets terrain bounds, lat/lon, idempotency). First test coverage for v3.1.0 surface — more coming in v3.2. ### v3.0.6 diff --git a/Documentation~/CameraSystem.md b/Documentation~/CameraSystem.md new file mode 100644 index 000000000..61282d3e1 --- /dev/null +++ b/Documentation~/CameraSystem.md @@ -0,0 +1,156 @@ + +# Camera System + +The Mapbox SDK includes a camera system for navigating the map with mouse and touch input. There are two camera modes, each suited to different use cases. + +--- + +## Camera Modes + +### 3D Camera (Moving3dCameraBehaviour) + +The camera moves through 3D space while the map stays in place. Best for: +- 3D city exploration +- Games and simulations +- Scenes where the camera needs to orbit around a point of interest + +The camera orbits a target point on the map. Panning moves the camera, zooming adjusts how far the camera is from the ground, and rotation orbits around the target. + +### Slippy Map Camera (SlippyMapCameraBehaviour) + +The camera stays in a fixed position while the map moves underneath. Best for: +- Traditional 2D map interfaces +- Navigation and routing views +- Data visualization overlays +- AR/VR applications where the camera is controlled by head tracking + +Panning shifts the map's geographic center. Zooming changes the map's scale level. The camera itself doesn't move — the world moves under it. + +--- + +## Setting Up + +1. Your scene needs a **MapboxMapBehaviour** on a GameObject (this is the map itself). +2. Add a camera behaviour component to any GameObject in the scene: + - **Moving3dCameraBehaviour** for 3D camera, or + - **SlippyMapCameraBehaviour** for slippy map camera +3. Assign the **Map Behaviour** field to the GameObject that has MapboxMapBehaviour. +4. Assign the **Camera** field to the scene camera used for rendering. If left empty, the main camera is used automatically. + +The camera will initialize itself once the map is ready. No additional setup is required. + +--- + +## Settings Reference + +### 3D Camera Settings + +| Setting | Description | +|---------|-------------| +| **Pitch** | Camera tilt angle. 90 = looking straight down, 15 = near the horizon. | +| **Bearing** | Camera rotation in degrees. 0 = north is up. | +| **Camera Curve** | An animation curve that controls how camera height changes with zoom level. The X axis is zoom, Y axis is distance from the ground. | +| **Distance Scale** | Multiplier on the camera curve output. Increase to raise the camera without re-editing the curve. Default: 2. | +| **Zoom Sensitivity** | How much each scroll step changes the zoom level. Default: 0.25. | +| **Rotation Speed** | How fast right-click drag rotates the camera. Default: 50. | + +### Slippy Map Camera Settings + +| Setting | Description | +|---------|-------------| +| **Center Latitude Longitude** | Initial map center as a geographic coordinate. Example: 40.7128, -74.0060 for New York. | +| **Pitch** | Camera tilt angle. 90 = looking straight down, 15 = near the horizon. | +| **Bearing** | Camera rotation in degrees. 0 = north is up. | +| **Camera Distance** | How far the camera is from the ground target. Typical range: 100 to 1000 depending on your map's scale. Default: 200. | +| **Pan Enabled** | Toggle left-click / single-finger drag to pan the map. | + +**Rotation Settings** (nested): + +| Setting | Description | +|---------|-------------| +| **Enabled** | Toggle right-click drag to rotate. | +| **Speed** | Rotation sensitivity. Default: 50. | +| **Rotation Mode** | *Rotate The Camera* orbits the camera around the map. *Rotate The Map* keeps the camera fixed and rotates the map underneath — useful for navigation-style views. | +| **Map Root** | The root transform of the map. Auto-detected if left empty. | + +**Zoom Settings** (nested): + +| Setting | Description | +|---------|-------------| +| **Enabled** | Toggle scroll wheel / pinch zoom. | +| **Speed** | Zoom sensitivity per step. Default: 0.25. | +| **Zoom At Cursor** | When enabled, the map zooms toward the cursor or pinch position. When disabled, zoom is always centered on the map. | + +--- + +## Input Controls + +Both cameras support mouse and touch input. The controls are: + +| Action | Mouse | Touch | +|--------|-------|-------| +| Pan | Left-click drag | Single-finger drag | +| Rotate | Right-click drag | Not available via touch | +| Zoom | Scroll wheel | Two-finger pinch | +| Tilt | Not available via mouse | Two-finger vertical drag | + +When using two fingers, the system automatically detects whether you are pinching (zoom) or dragging vertically (tilt) and applies the dominant gesture. Both gestures will not activate at the same time. + +Zoom at cursor works with both mouse and touch — when using pinch, the zoom centers on the midpoint between your two fingers. + +--- + +## Input System Support + +The cameras work with both Unity's legacy Input Manager and the new Input System package. `com.unity.inputsystem` is a hard dependency of the SDK (UPM installs it automatically), but the active code path is selected by Player Settings, not by package presence: + +- **Active Input Handling = "Input Manager (Old)" (default)** — uses the legacy `UnityEngine.Input` API. No action needed, and this is unchanged by the package being installed. +- **Active Input Handling = "Input System Package (New)" or "Both"** — Unity defines the built-in `ENABLE_INPUT_SYSTEM` symbol, which activates the new-input branch. Pointer state is read via `Mouse.current` / `Touch.activeTouches`. + +Installing `com.unity.inputsystem` alone does not change which path runs — only flipping Active Input Handling does. See the [CHANGELOG](../CHANGELOG.md#v311) for why the SDK moved from a soft dependency gated on package presence to this model. + +--- + +## Writing a Custom Camera + +If neither camera mode fits your needs, you can create your own by writing a class that extends `MapInput` and a MonoBehaviour that extends `MapCameraBehaviour`. + +Your custom camera class needs to implement one method — `UpdateCamera` — where you handle input and return camera state (center, zoom, pitch, bearing, scale). The base class provides ready-made helpers for input detection, coordinate conversion, and value clamping. + +The behaviour wrapper handles initialization and communicating changes to the map automatically. A minimal custom camera behaviour only needs a few lines of code beyond the camera logic itself. + +--- + +## AR / MR setups + +As of v3.1.0, the SDK supports parenting the map under an external transform — an `ARAnchor`, an XR rig, or any rotated/translated parent — and having the entire map (terrain tiles, buildings, roads, areas) compose correctly with that parent transform. This makes it possible to place the map on a real-world surface in AR, anchor it to a tracked plane, or rotate the whole map as a unit. + +### What works + +- **Translating `MapRoot`** in world space — the whole map moves as a unit. +- **Rotating `MapRoot`** around any axis — terrain and vector content follow. +- **Parenting `MapRoot`** under an `ARAnchor`, an `XROrigin` child, or any custom rig — the parent's translation and rotation propagate. + +All of this works because tile placement and vector-content placement are done in **local space** relative to `MapRoot`, and runtime-created roots use `SetParent(parent, worldPositionStays: false)` so they don't pin themselves to world coordinates. + +### What doesn't work (yet) + +- **Non-unit `MapRoot.localScale`.** The quadtree LOD math (`UnityTileProvider.ShouldSplit`) reads the camera's world-space position and compares against tile bounds derived from `MapInformation.Scale`. Scaling `MapRoot` doesn't propagate to that math, so the LOD will pick zoom levels appropriate for the unscaled tile size, not the visible one. + + **Use `MapInformation.Scale` to drive map size instead.** A `Scale` of 1000 means each Web Mercator unit is rendered as 1000 Unity units. For a tabletop-size diorama, set `Scale` to a small value (e.g. 0.01) — the LOD math will follow. For an animated tabletop → world-size transition, the `DynamicScalingMapInformation` class drives `Scale` from an animation curve indexed by zoom. + +### Recommended setup + +For AR/MR apps you generally want: + +1. **Replace the built-in camera behaviours.** `SlippyMapCameraBehaviour` and `Moving3dCameraBehaviour` both write directly to `Camera.transform` every frame (and in *Rotate The Map* mode, `SlippyMapCamera` writes to `MapRoot.rotation`). In an AR/MR setup ARKit/ARCore is already driving the camera transform; you don't want the SDK fighting it. Remove these components from the scene. + +2. **Write a thin custom `MapCameraBehaviour`** that does the inverse: instead of moving the camera based on input, it reads the AR-tracked camera's pose and pushes the relevant state into `MapboxMap.ChangeView(...)`. You typically only need to push `Center` (lat/lon) and `Scale` — pitch and bearing can stay at their initial values since the AR camera handles its own orientation. + +3. **Let the quadtree LOD see the AR camera.** `UnityTileProvider.Settings.Camera` can be set explicitly via the `UnityTileProviderBehaviour` inspector. Point it at your AR camera. The tile cover will frustum-cull against the AR camera's actual view. + +4. **Animate scale, not transform.** For a "diorama grows to world size" effect, lerp `MapInformation.Scale` over time. The visualizer reacts to `WorldScaleChanged` and re-lays-out tiles without re-fetching as long as zoom is held constant. Pre-warm the cache for the destination zoom range if you cross zoom boundaries during the animation. + +### Limitations + +This is the first release with AR-style parent transforms supported architecturally. It hasn't been validated against a full ARKit / ARCore sample, and it has not been profiled on AR-class devices. Treat it as **early access**: the bones are in place, but expect to do your own integration work and report rough edges. diff --git a/Documentation~/ChangeImageryStyleOnRuntime.md b/Documentation~/ChangeImageryStyleOnRuntime.md new file mode 100644 index 000000000..9898d7905 --- /dev/null +++ b/Documentation~/ChangeImageryStyleOnRuntime.md @@ -0,0 +1,28 @@ +# Change Imagery Style at Runtime + +To change the imagery style at runtime, follow these steps: + +1. Get a reference to the `MapboxMap`. +2. Access the `StaticApiLayerModule` from the `MapVisualizer`. +3. Change the `tilesetId` (asynchronous operation). +4. Reload all currently active tiles to apply the new imagery. + +Changing the `tilesetId` is not instantaneous, so it must be handled as a coroutine. After the update completes, explicitly reload the active tiles to refresh the visible content. + +```csharp +private IEnumerator ReloadImageModule(MapboxMap map, string tilesetId) +{ + if (map.MapVisualizer.TryGetLayerModule(out var imageModule)) + { + yield return imageModule.ChangeTilesetId(tilesetId); + + foreach (var tilePair in map.MapVisualizer.ActiveTiles) + { + var tile = tilePair.Value; + imageModule.LoadInstant(tile); + } + } +} +``` + +This ensures the imagery source is updated and all visible tiles are refreshed immediately without requiring user interaction. \ No newline at end of file diff --git a/Documentation~/ComponentsModule.md b/Documentation~/ComponentsModule.md new file mode 100644 index 000000000..1ae8c1dbb --- /dev/null +++ b/Documentation~/ComponentsModule.md @@ -0,0 +1,327 @@ +11## Components Module + +The Components Module is a high-performance alternative to the Vector Layer Module, designed for rendering large-scale maps with significantly reduced memory allocations and improved frame rates. + +Like the Vector Layer Module, it uses the [Mapbox Vector Tiles API](https://docs.mapbox.com/api/maps/vector-tiles/) to fetch map data. However, it processes this data through a completely redesigned pipeline optimized for zero-allocation mesh generation and background threading. + +--- + +### Why Components Module Exists + +The original Vector Layer Module, while flexible, was not optimized for performance-critical applications. The Components Module was created to address performance limitations through: + +**Performance Improvements:** +- **Custom PBF Parser**: A zero-allocation Protocol Buffer parser using `ref struct` and `Span` for direct memory access +- **Optimized Data Structures**: Pre-sized buffers and stack-allocated arrays minimize GC pressure +- **Merged Meshes**: Buildings and roads within each tile are combined into single meshes, dramatically reducing draw calls compared to the Vector Layer Module's per-feature mesh approach + +--- + +### Comparison: Components Module vs Vector Layer Module + +| Feature | Vector Layer Module | Components Module | +|---------|---------------------|-------------------| +| **Flexibility** | Highly flexible with modifier stacks | Fixed pipeline per component type | +| **Extensibility** | Easy to add custom modifiers | Requires C# coding to extend | +| **Mesh Output** | Individual meshes per feature | Merged meshes per tile (buildings/roads combined) | +| **Performance** | Adequate for small-medium maps | Optimized for large-scale maps | +| **Memory** | Moderate allocation rate | Minimal allocations | + +**When to Use Components Module:** +- Large maps with many visible tiles +- Mobile or low-end hardware targets +- Applications requiring stable 60 FPS +- Dense urban environments with thousands of buildings + +**When to Use Vector Layer Module:** +- Rapid prototyping +- Custom rendering workflows +- Non-performance-critical applications +- Need for runtime modifier configuration + +--- + +### Architecture Overview + +The Components Module processes vector tiles through these stages: + +1. **Tile Download**: Vector tiles are fetched and decompressed (shared with Vector Layer Module) +2. **Background Parsing**: Custom PBF parser decodes tile data on background threads using zero-allocation techniques +3. **Mesh Generation**: Component visualizers generate meshes in background tasks using `Span` for direct vertex manipulation +4. **Main Thread Finalization**: Generated meshes are transferred to GameObjects on the main thread + +**Key Technical Components:** +- `VectorTileReader`: Zero-allocation PBF parser using `ref struct` and `ReadOnlySpan` +- `PbfReader`: Low-level varint decoder with optimized packed array handling +- `MapboxComponentVisualizer`: Base class for component visualizers with stable ID assignment +- `MapboxComponentsModule`: Coordinates background mesh generation and main-thread finalization +- `ArrayPolygon`, `ArrayHeight`, `ArrayChamferHeight`: Stack-optimized mesh modifiers + +--- + +### Module Setup + +To use the Components Module instead of the Vector Layer Module: + +1. Create a new `MapboxComponentsModuleScript` component on your map GameObject +2. Configure the module settings (similar to Vector Layer Module): + - **Source Type**: Mapbox Streets V8 or custom tileset + - **Cache Size**: Number of tiles to keep in memory + - **Clamp Data Level**: Maximum zoom level for data fetching + - **Reject Tiles Outside Zoom**: Discard tiles outside zoom range + +3. Add component visualizers: + - **Building Component**: For rendering 3D buildings + - **Road Component**: For rendering road networks + - **Area Component**: For rendering land use, water, parks, etc. + +--- + +### Component Visualizers + +The Components Module provides three specialized visualizers, each optimized for a specific feature type. + +#### 1. Building Component Visualizer + +Renders 3D buildings from the `building` layer. + +**Configuration Options:** + +- **Material** + The material applied to all building meshes in this component. + +- **Enable Terrain Snapping** + When enabled, building vertices are adjusted to match terrain elevation. + Requires a terrain-enabled map. Useful for hilly or mountainous areas. + +- **Round Building Corners** + Applies chamfered (rounded) corners to building geometry. + Creates more realistic building shapes but increases vertex count. + +- **Extrusion Settings** + Controls how building heights are determined: + - **Property Height**: Uses the `height` property from vector data (default) + - **Absolute Height**: Sets all buildings to a fixed height + - **Max Height**: Caps building heights at a maximum value + - **Min Height**: Ensures buildings are at least a minimum height + - **Range Height**: Clamps heights between min and max values + - **Extrusion Scale Factor**: Multiplies all height values (e.g., 0.5 for half height) + +- **Chamfer Offset** (when Round Building Corners is enabled) + The distance in meters to inset corners. Typical values: 0.5 - 2.0 meters. + +**Creating a Building Component Visualizer:** +1. Right-click in Project window: `Create → Mapbox → Layer Visualizers → Building Component Visualizer` +2. Configure the settings +3. Add the visualizer asset to your `MapboxComponentsModuleScript` + +--- + +#### 2. Road Component Visualizer + +Renders road networks from the `road` layer with style-based filtering. + +**Configuration Options:** + +- **Game Object Offset** + Vertical offset applied to the entire road layer. + Useful for raising roads slightly above terrain to prevent z-fighting. + +- **Random Offset Range** + Small random vertical offset per road segment (default: 0.0001). + Prevents z-fighting when multiple road segments overlap. + +- **Road Style Sheet** + A `RoadStyleSheet` asset that defines how different road types are rendered. + +**Road Style Sheet:** + +A Road Style Sheet contains multiple road styles, each with: + +- **Name**: Descriptive name (for organization only) +- **Filters**: Determines which road features this style applies to +- **Width**: Road width in meters +- **Push Up**: Additional vertical offset for this style +- **Material**: Material applied to roads matching this style + +**Filter Stack:** + +Filters determine which roads match a style. Common filters: +- **Class Filter**: Match by road class (e.g., "motorway", "primary", "residential") +- **Type Filter**: Match by road type (e.g., "trunk", "link") +- **Structure Filter**: Match by structure (e.g., "bridge", "tunnel") + +Filters can be combined using: +- **Any**: Feature matches if any filter passes +- **All**: Feature matches only if all filters pass +- **None**: Feature matches if no filters pass + +**Example Style Setup:** +``` +Style: "Highways" + Filters: Class = "motorway", Type = "Any" + Width: 12.0 + Material: HighwayMaterial + +Style: "Main Roads" + Filters: Class = "primary" OR "secondary" + Width: 8.0 + Material: MainRoadMaterial + +Style: "Local Streets" + Filters: Class = "residential" + Width: 4.0 + Material: LocalStreetMaterial +``` + +**Creating a Road Component Visualizer:** +1. Right-click in Project: `Create → Mapbox → Road Style Sheet` +2. Configure styles and filters in the sheet +3. Right-click: `Create → Mapbox → Layer Visualizers → Road Component Visualizer` +4. Assign the style sheet to the visualizer +5. Add visualizer to `MapboxComponentsModuleScript` + +--- + +#### 3. Area Component Visualizer + +Renders polygonal areas such as water, parks, landuse, etc. + +**Configuration Options:** + +- **Layer Name** + The vector layer to render (e.g., "water", "landuse", "park"). + Unlike building/road components which are fixed to their layers, area components are generic and can target any polygon layer. + +- **Game Object Offset** + Vertical offset applied to the entire area layer. + Useful for layering (e.g., water at 0, parks at 0.1, landuse at 0.2). + +- **Styles** + A list of area styles, each defining how features with a specific class are rendered. + +**Area Style:** + +Each area style has: +- **Class Name**: The feature class to match (e.g., "park", "residential", "commercial") +- **Material**: Material applied to matching features +- **Push Up**: Additional vertical offset for this class + +**Example Setup for Landuse:** +``` +Layer Name: "landuse" +Styles: + - Class: "residential", Material: ResidentialMat, Push Up: 0.0 + - Class: "commercial", Material: CommercialMat, Push Up: 0.1 + - Class: "industrial", Material: IndustrialMat, Push Up: 0.2 + - Class: "park", Material: ParkMat, Push Up: 0.3 +``` + +**Example Setup for Water:** +``` +Layer Name: "water" +Styles: + - Class: "*", Material: WaterMat, Push Up: 0.0 +``` +(Water features typically don't have class variations, so a single catch-all style works) + +**Creating an Area Component Visualizer:** +1. Right-click: `Create → Mapbox → Layer Visualizers → Area Component Visualizer` +2. Set the layer name +3. Add styles for each class you want to render +4. Add visualizer to `MapboxComponentsModuleScript` + +--- + +### Best Practices + +**Material Configuration:** +- Use shared materials across components to reduce draw calls +- Enable GPU instancing on materials when possible +- Consider mobile shader variants for cross-platform apps + +**Performance Tuning:** +- Use **Clamp Data Level** to limit detail at high zoom (e.g., clamp to 15 for dense cities) +- Reduce **Cache Size** if memory is constrained (default is usually fine) +- Disable **Terrain Snapping** if terrain isn't used (saves CPU) +- Use **Random Offset Range** sparingly—smaller values are better + +**Styling Tips:** +- Keep road style sheets simple—fewer styles mean faster filtering +- Use **Push Up** offsets to layer features (water → roads → buildings) +- Test materials with both day/night lighting for consistent appearance + +--- + +### Limitations + +The Components Module sacrifices flexibility for performance: + +1. **Fixed Component Types**: Only buildings, roads, and areas are supported out of the box +2. **Less Extensible**: Adding new component types requires C# programming and understanding of the performance architecture +3. **No Per-Feature Callbacks**: Vector Layer Module's per-feature game object modifiers aren't available +4. **Simplified Filtering**: Road/area filtering is less powerful than full modifier stacks + +For most production use cases, these limitations are acceptable trade-offs for the performance gains. If you need the flexibility, use the Vector Layer Module instead. + +--- + +### Troubleshooting + +**Buildings don't appear:** +- Verify Building Component Visualizer is active (`IsActive = true`) +- Check material is assigned +- Ensure map zoom level is 13+ (building data starts at zoom 13) +- Verify tileset includes `building` layer + +**Roads render incorrectly:** +- Check Road Style Sheet is assigned +- Verify styles have filters that match road classes in your area +- Test with a single catch-all style first: `Filters: Class = Any` +- Confirm road width is reasonable (1-20 meters) + +**Z-fighting between layers:** +- Increase `GameObjectOffset` for each component (e.g., roads: 0.1, areas: 0.0) +- Increase `RandomOffsetRange` for roads if segments overlap +- Use `PushUp` to layer features within a component + +**Performance still poor:** +- Profile with Unity Profiler—confirm mesh generation is the bottleneck +- Reduce visible tile count via camera distance/zoom constraints +- Lower `CacheSize` if memory is limited (won't improve FPS but reduces memory) +- Simplify Road Style Sheet—fewer styles = faster filtering +- Disable terrain snapping if not needed + +--- + +### API Events + +Components expose events for integration with custom systems: + +**BuildingComponentVisualizer:** +- `OnBuildingMeshCreated(CanonicalTileId tileId, GameObject gameObject, MeshData meshData)`: Fired when a building tile mesh is created +- `OnBuildingMeshDestroyed(CanonicalTileId tileId, GameObject gameObject)`: Fired when a building tile is unregistered + +**RoadComponentVisualizer:** +- `OnRoadMeshCreated(CanonicalTileId tileId, GameObject gameObject, MeshData meshData)`: Fired when a road tile mesh is created +- `OnRoadMeshDestroyed(CanonicalTileId tileId, GameObject gameObject)`: Fired when a road tile is unregistered + +**AreaComponentVisualizer:** +- `OnAreaMeshCreated(CanonicalTileId tileId, GameObject gameObject, MeshData meshData)`: Fired when an area tile mesh is created +- `OnAreaMeshDestroyed(CanonicalTileId tileId, GameObject gameObject)`: Fired when an area tile is unregistered + +**Usage Example:** +```csharp +buildingVisualizer.OnBuildingMeshCreated += (tileId, gameObject, meshData) => { + // Add custom components, colliders, etc. + gameObject.AddComponent(); +}; +``` + +--- + +### Conclusion + +The Components Module provides a high-performance alternative to the Vector Layer Module for production applications that need stable frame rates with large-scale maps. While it sacrifices some flexibility, the performance gains—particularly on mobile and low-end hardware—make it the preferred choice for performance-critical projects. + +For rapid prototyping or custom rendering workflows, the Vector Layer Module remains the better option. Choose the module that best fits your project's needs. diff --git a/Documentation~/GettingStartedWithMapboxMapObject.md b/Documentation~/GettingStartedWithMapboxMapObject.md index 0abd64423..75a55a2a6 100644 --- a/Documentation~/GettingStartedWithMapboxMapObject.md +++ b/Documentation~/GettingStartedWithMapboxMapObject.md @@ -88,3 +88,56 @@ To create a basic map in your own Unity scene, follow these simple steps: --- + +## Common Tasks + +### Place a GameObject at a latitude / longitude + +To position your own object (a marker, a character avatar, a custom POI) at a specific geographic coordinate, use `IMapInformation.ConvertLatLngToPosition`: + +```csharp +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.Example.Scripts.Map; +using UnityEngine; + +public class PlaceMarker : MonoBehaviour +{ + public MapboxMapBehaviour MapBehaviour; + public Transform Marker; + + private void Start() + { + // Wait until the map is ready, then place the marker. + MapBehaviour.MapboxMap.Initialized += OnMapReady; + } + + private void OnMapReady() + { + var coord = new LatitudeLongitude(40.7484, -73.9857); // Empire State Building + var localPos = MapBehaviour.MapboxMap.MapInformation.ConvertLatLngToPosition(coord); + + // Parent under the map root so the marker composes with any transform applied + // to the map hierarchy (rotation, translation, AR anchor, etc.). + Marker.SetParent(MapBehaviour.UnityContext.MapRoot, worldPositionStays: false); + Marker.localPosition = localPos; + } +} +``` + +**Important**: the value `ConvertLatLngToPosition` returns is in the **map's local coordinate system**, not world space. Always assign it to `localPosition` of a Transform parented under `UnityContext.MapRoot`, never to `transform.position`. This composes correctly with any parent transformations (useful for AR/MR scenarios where the map is anchored to a real-world surface). + +For optional **terrain elevation** (drop your object onto the terrain surface instead of at Y=0), use `MapboxMap.TryGetElevation(latlng, out float elevation)`: + +```csharp +if (MapBehaviour.MapboxMap.TryGetElevation(coord, out var elevation)) +{ + localPos.y = elevation; +} +Marker.localPosition = localPos; +``` + +If the map moves (via `MapboxMap.ChangeView`, `MapInformation.SetInformation`, or `ShiftMapForDistance`), object positions need to be re-computed. Subscribe to `IMapInformation.ViewChanged` and re-run the conversion when it fires. + +See also `Documentation~/CoordinateConversions.md` for the full coordinate-conversion contract, and `Documentation~/WorkingWithPois.md` if your data lives in a Mapbox vector tile layer (e.g. `poi_label`) rather than from your own backend. + +--- diff --git a/Documentation~/Migration-3.0-to-3.1.md b/Documentation~/Migration-3.0-to-3.1.md new file mode 100644 index 000000000..bb9d6db02 --- /dev/null +++ b/Documentation~/Migration-3.0-to-3.1.md @@ -0,0 +1,169 @@ +# Migration guide — v3.0 → v3.1 + +v3.1.0 is a substantial release that introduces a new camera system, a new tile-provider LOD algorithm, a Burst-compiled terrain pipeline, and a multicast cache disposal contract. Most projects will upgrade with no source changes. A handful of API shapes have changed; this document walks through each so you know exactly what to touch. + +For the full release notes see [`CHANGELOG.md`](../CHANGELOG.md). This guide is the *narrative upgrade path* — the CHANGELOG is the reference. + +--- + +## Before you upgrade + +1. Commit your working state. +2. Note your **Managed Stripping Level** in Player Settings → Android → Other Settings → Optimization. v3.1.0's tested target is **Low** (or Minimal / Disabled). **Medium** has been briefly tested and seems to work — the SDK now preserves its SQLite model classes with `[Preserve]` attributes plus a `link.xml`. **High** is untested and *might* work given the same preservation, but no guarantees. If you were on Low or below, stay there. If you want to try Medium / High, validate on your build before committing. +3. If your project pinned a specific Burst version, note it — v3.1.0 adds `com.unity.burst@1.8.12` as a new dependency. + +--- + +## Step 1 — Install v3.1.0 + +Replace the v3.0 package with v3.1.0 via Package Manager (or re-clone the repo into your `Packages` folder). The first launch will trigger Burst AOT compilation (one-shot, ~10–60s depending on the platform). Subsequent launches are normal. + +The new dependency is resolved automatically by UPM: + +| Dependency | Status | Why | +| --- | --- | --- | +| `com.unity.burst@1.8.12` | **New required dep** | Terrain-RGB decode and collider vertex fill are now Burst jobs. | +| `com.unity.inputsystem@1.7.0` | **New required dep (as of v3.1.1)** | Hard dependency, auto-installed by UPM. Which input path runs is controlled by Player Settings → Active Input Handling, not by package presence — see [CameraSystem.md](CameraSystem.md#input-system-support). Projects on "Input Manager (Old)" (the default) see no behavior change. | + +--- + +## Step 2 — Source-level breaks to fix + +The compile errors you might see are all narrow. If a section doesn't apply, your project doesn't trip the break. + +### Custom `IMapInformation` implementations + +If you ship your own `IMapInformation` (i.e., not just using the built-in `MapInformation`), the interface gained a new property: + +``` +public TerrainInfo Terrain { get; } +``` + +Add it. For non-terrain maps, return `new TerrainInfo()` — defaults (`MinElevation = 0`, `MaxElevation = 5000`) are fine. The built-in `MapInformation` already implements it. + +### Direct assignment to `TerrainData.ElevationValuesUpdated` + +The field is now a proper `event Action`. Old code that did `data.ElevationValuesUpdated = myCallback` no longer compiles, nor does `data.ElevationValuesUpdated()` to fire it. Subscribe with `+=` and unsubscribe with `-=`. The previous helper `SetElevationChangedCallback` (which silently wiped other subscribers) is removed. + +### Calls to `MapboxTileData.SetDisposeCallback` + +Removed in favor of multicast: + +- `AddDisposeCallback(Action)` — register a handler +- `RemoveDisposeCallback(Action)` — deregister + +Up to 16 render tiles share a single `TerrainData` instance; the old single-setter API silently dropped 15 of them on every replacement. If you assigned via reflection or `InternalsVisibleTo`, switch to the public add/remove pair. + +### Subclasses of `MapInput` + +If you wrote a custom `MapInput` (e.g., to add input behaviours not in the built-in set), you should override the new virtual: + +``` +public override void Teardown(IMapInformation mapInfo) +{ + // Unsubscribe anything you wired in Initialize +} +``` + +It's a no-op by default — you won't see a compile error, but without the override your subscriptions root the destroyed camera + its Transform when `IMapInformation` outlives the camera (DontDestroyOnLoad / scene reload). + +### Subclasses of `UnityTileProvider` + +`TileNode.Set` signature changed: + +- Old: `Set(id, worldCenter, scale, boundsHeight)` +- New: `Set(id, worldCenter, scale, boundsBottom, boundsHeight)` + +Update the call site if you subclassed and reached into `TileNode` directly. The new `boundsBottom` lets the AABB reach sub-sea-level for terrain that dips below Y=0 (Death Valley, Dead Sea). + +### Subclasses of `CustomTMSTile` + +The constructor went from 4 args to 6: + +- Old: `(urlFormat, tileId, tilesetId, useNonReadableTexture)` +- New: `(urlFormat, tileId, tilesetId, useNonReadableTexture, invertY, isMapboxService)` + +The 4-arg constructor is preserved as an overload that defaults `invertY: true, isMapboxService: false` — your existing subclasses still compile. New code should use the 6-arg form to control the two booleans explicitly. + +### Camera behaviours subclassed from `MapCameraBehaviour` + +There's now an open generic abstract base `MapCameraBehaviour` where `T` is the camera-core type. The shipped behaviours (`SlippyMapCameraBehaviour`, `Moving3dCameraBehaviour`) close the generic. If you subclassed the old non-generic base, change to: + +``` +public class MyCustomBehaviour : MapCameraBehaviour { ... } +``` + +--- + +## Step 3 — Behavioral changes to verify + +These don't cause compile errors but may change runtime behavior. + +### Custom imagery: `CustomSource.CreateTile` always returns `CustomTMSTile` when `UrlFormat` is set + +Previously `InvertY=false` with a non-empty `UrlFormat` would silently fall back to a plain `RasterTile`. v3.1.0 always uses `CustomTMSTile`. If you depended on the plain-raster path, set `UrlFormat = ""` — empty `UrlFormat` falls back to plain `RasterTile` for both `CustomSource` and `CustomTerrainSource`. + +### Custom terrain: `Settings.DataSettings` now actually applies + +`CustomTerrainLayerModuleScript` used to throw away the Inspector-configured `DataSettings` and use defaults. v3.1.0 passes them through. **If you had Inspector values set, they now take effect** — cache size, retina flag, non-readable textures, data-zoom clamp. Verify they were intentional. + +### `TerrainInfo.IsEnabled` and `Exaggeration` removed + +Both fields were written-but-never-read in v3.0. v3.1.0 drops them. If your code set either field, the assignment no longer compiles — but the runtime behavior is unchanged, since nothing consumed those fields anyway. Delete the assignments. + +### Tile provider — new defaults, new clamps + +- `UnityTileProviderSettings.SubdivisionBias` default changed from `1.0f` to `0.6f`. New scenes get `0.6` as the inspector default. Legacy scenes saved before this field existed deserialize to `0` (Unity assigns `default(float)` for new fields and skips C# initializers); `ISerializationCallbackReceiver.OnAfterDeserialize` upgrades anything `≤ 0` to `0.6f` automatically. +- `UnityTileProvider.MaximumZoomLevel` is now silently clamped to **30** internally with a log warning if higher. Practically unreachable in Mercator but worth knowing. + +### `MapInformation.Initialize` resets terrain bounds + +`Initialize(LatitudeLongitude)` now resets `Terrain.MinElevation` / `MaxElevation` to `TerrainInfo` defaults. If you intentionally pre-set these before `Initialize` and relied on them surviving, move that assignment to *after* `Initialize`. New `MapInformation` instances behave identically. + +### Tile placement is now local-space + +Tiles are positioned via `transform.localPosition` (was `transform.position`). On every existing scene with `MapRoot` at the world origin with identity rotation and unit scale this is *identical* behaviour. If your project transforms `MapRoot` (AR anchor, custom rig), the SDK now respects that translation and rotation. **Non-unit `MapRoot.localScale` is not supported** — drive map scale through `MapInformation.Scale` instead. See `Documentation~/CoordinateConversions.md` for the local-space conversion contract. + +--- + +## Step 4 — Camera setup + +v3.1.0 replaces the old camera setup with two concrete behaviours plus the generic base. Most projects use one of the shipped behaviours unchanged: + +- `SlippyMapCameraBehaviour` — fixed camera, map moves underneath. Good for 2D-style maps, navigation overlays, AR/VR. +- `Moving3dCameraBehaviour` — camera orbits a target. Good for 3D city exploration, games. + +Touch support is automatic on iOS/Android device builds (compile-time platform split via `(UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR`). No project-side configuration. Touch path can't be tested in the Editor — see `Documentation~/CameraSystem.md` for the workaround. + +--- + +## Step 5 — Default tile material (Android-relevant) + +`MapboxMapBehaviour` has a new serialized `Default Tile Material` field under the **Settings** foldout in its custom Inspector. When no `TileCreatorBehaviour` is assigned, this Material is used as the fallback. + +For scenes built from scratch in v3.1.0, drag `ElevatedTerrainMaterial.mat` (`Runtime/Mapbox/BaseModule/Unity/Visuals/Materials/`) into this field. The demo scenes ship with it pre-wired. + +For scenes upgraded from v3.0 without a `TileCreatorBehaviour`, the previous fallback path used `Shader.Find` — which is unreliable in player builds (the shader gets stripped unless it's in *Always Included Shaders* or referenced from a shipped Material). v3.1.0 throws an `InvalidOperationException` with actionable instructions if both fields are null. Wire the Material reference and you're done. + +--- + +## Step 6 — Rebuild + +1. Clear the Library/ cache if Unity gets confused after the dependency change (rare but possible with Burst's first AOT pass). +2. Editor playmode should work without further changes. +3. iOS build: no project-side changes needed beyond making sure your `Info.plist` has `NSLocationWhenInUseUsageDescription` (unchanged from v3.0). +4. Android build: any Managed Stripping Level works. The SQLite cache model classes are preserved via per-property `[Preserve]` attributes and a backup `Plugins/link.xml` declaration — both shipped in the package. + +--- + +## Verification checklist + +- [ ] Project compiles +- [ ] Sample scene (LocationBasedGame or WorldMapViewer) renders identically to v3.0 +- [ ] Touch pan / pinch zoom / two-finger tilt all work on device +- [ ] iOS build runs and renders tiles +- [ ] Android build runs and renders tiles, **with the SQLite cache populating** (search logcat for `Error inserting … (extended=ConstraintForeignKey)` — should be absent) +- [ ] If you customized `IMapInformation`, `MapInput`, `UnityTileProvider`, `CustomTMSTile`, or `MapCameraBehaviour`: the relevant subclasses still compile and behave correctly +- [ ] If you used `TerrainData.ElevationValuesUpdated` directly or `MapboxTileData.SetDisposeCallback`: migrated to the new event / multicast pattern + +If any of the above fails, check the [Reporting issues](../README.md#reporting-issues--contributing) section and file with reproduction details. diff --git a/Documentation~/UsingDirectionsApi.md b/Documentation~/UsingDirectionsApi.md new file mode 100644 index 000000000..21775b731 --- /dev/null +++ b/Documentation~/UsingDirectionsApi.md @@ -0,0 +1,113 @@ + +# Using the Mapbox Directions API + +The Mapbox Directions API allows you to calculate routes between locations. You can get turn-by-turn directions, route geometry, distance, and duration for different routing profiles (driving, walking, cycling). + +For complete API reference and details, see the [official Mapbox Directions API documentation](https://docs.mapbox.com/api/navigation/directions/). + +## Basic Setup + +To use the Directions API, you need to: + +1. Reference the `MapBehaviourCore` component to access the map's file source. +2. Create a `MapboxDirectionsApi` instance with the file source. +3. Build a `DirectionResource` with your origin and destination coordinates. +4. Query the API and handle the response in a callback. + +Here's a simple example: + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.DirectionsApi; +using Mapbox.DirectionsApi.Response; +using Mapbox.Utils; + +public class DirectionsExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxDirectionsApi _directionsApi; + + void Start() + { + MapCore.Initialized += map => + { + _directionsApi = new MapboxDirectionsApi(map.MapService.FileSource); + GetRoute(); + }; + } + + void GetRoute() + { + // Define two coordinates: San Francisco to Los Angeles + var origin = new LatitudeLongitude(37.7749, -122.4194); + var destination = new LatitudeLongitude(34.0522, -118.2437); + + // Create a directions request with driving profile + var directionResource = new DirectionResource( + new[] { origin, destination }, + RoutingProfile.Driving + ); + + // Query the API + _directionsApi.Query(directionResource, response => + { + if (response == null || response.Routes == null || response.Routes.Count == 0) + { + Debug.Log("No route found"); + return; + } + + var route = response.Routes[0]; + Debug.Log($"Route found!"); + Debug.Log($"Distance: {route.Distance} meters"); + Debug.Log($"Duration: {route.Duration} seconds"); + Debug.Log($"Geometry points: {route.Geometry.Count}"); + }); + } +} +``` + +## What This Does + +- The script waits for the map to initialize and gets access to the file source. +- Creates a `MapboxDirectionsApi` instance for making routing queries. +- Defines a route from San Francisco to Los Angeles using latitude/longitude coordinates. +- Requests a driving route between the two points. +- When the response arrives, it logs the route distance (in meters), duration (in seconds), and number of geometry points. + +## Routing Profiles + +You can choose from three routing profiles: + +- `RoutingProfile.Driving` - Optimized for car travel +- `RoutingProfile.Walking` - Optimized for pedestrians +- `RoutingProfile.Cycling` - Optimized for bicycle travel + +## Optional Parameters + +The `DirectionResource` supports several optional parameters: + +```csharp +var directionResource = new DirectionResource(coordinates, RoutingProfile.Driving) +{ + Alternatives = true, // Get alternative routes + Steps = true, // Include turn-by-turn instructions + Overview = Overview.Full // Get full route geometry +}; +``` + +## Response Data + +The `DirectionsResponse` contains: +- `Routes` - List of possible routes (usually one, unless alternatives are requested) +- `Waypoints` - Snapped waypoint locations +- `Code` - Response status code ("Ok" if successful) + +Each `Route` includes: +- `Distance` - Total route distance in meters +- `Duration` - Estimated travel time in seconds +- `Geometry` - List of lat/lng coordinates forming the route path +- `Weight` - Route weight (typically duration-based) + +--- diff --git a/Documentation~/UsingGeocodingApi.md b/Documentation~/UsingGeocodingApi.md new file mode 100644 index 000000000..1bdf96106 --- /dev/null +++ b/Documentation~/UsingGeocodingApi.md @@ -0,0 +1,195 @@ + +# Using the Mapbox Geocoding API + +The Mapbox Geocoding API provides two main functions: +- **Forward Geocoding**: Convert addresses or place names into geographic coordinates (lat/lng) +- **Reverse Geocoding**: Convert geographic coordinates into human-readable addresses + +For complete API reference and details, see the [official Mapbox Geocoding API documentation](https://docs.mapbox.com/api/search/geocoding/). + +## Forward Geocoding + +Forward geocoding converts a text query (like "San Francisco" or "1600 Pennsylvania Avenue") into coordinates. + +### Basic Example + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.GeocodingApi; + +public class ForwardGeocodeExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + MapCore.Initialized += map => + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + SearchPlace("San Francisco, CA"); + }; + } + + void SearchPlace(string placeName) + { + var forwardGeocodeResource = new ForwardGeocodeResource(placeName); + + _geocodingApi.Geocode(forwardGeocodeResource, (ForwardGeocodeResponse response) => + { + if (response == null || response.Features == null || response.Features.Count == 0) + { + Debug.Log("No results found"); + return; + } + + var firstResult = response.Features[0]; + Debug.Log($"Found: {firstResult.PlaceName}"); + Debug.Log($"Coordinates: {firstResult.Center.x}, {firstResult.Center.y}"); + }); + } +} +``` + +### What This Does +- Waits for the map to initialize +- Creates a geocoding request for "San Francisco, CA" +- Returns the place name and coordinates (latitude, longitude) +- The `Center` property contains the location as a `Vector2d` with x=latitude, y=longitude + +### Optional Parameters + +You can refine your forward geocoding searches: + +```csharp +var forwardGeocodeResource = new ForwardGeocodeResource("pizza") +{ + Country = new[] { "us" }, // Limit to United States + Proximity = new Vector2d(37.7, -122.4), // Bias results near San Francisco + Autocomplete = true, // Enable autocomplete suggestions + Types = new[] { "poi" } // Only return points of interest +}; +``` + +### Response Data + +The `ForwardGeocodeResponse` contains: +- `Features` - List of matching places (usually ordered by relevance) +- `Query` - The original query string +- `Attribution` - Mapbox attribution text + +Each `Feature` includes: +- `PlaceName` - Full address or place name +- `Text` - Short name of the place +- `Center` - Coordinates as Vector2d (x=lat, y=lng) +- `PlaceType` - Type of place (e.g., "address", "poi", "place") +- `Address` - Street address (if applicable) +- `Relevance` - Match score (0-1) +- `Context` - Additional location context (city, state, country, etc.) + +--- + +## Reverse Geocoding + +Reverse geocoding converts coordinates into a human-readable address or place name. + +### Basic Example + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.GeocodingApi; +using Mapbox.Utils; + +public class ReverseGeocodeExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + MapCore.Initialized += map => + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + + // Reverse geocode the Golden Gate Bridge location + var location = new LatitudeLongitude(37.8199, -122.4783); + GetAddressFromCoordinates(location); + }; + } + + void GetAddressFromCoordinates(LatitudeLongitude location) + { + var reverseGeocodeResource = new ReverseGeocodeResource(location); + + _geocodingApi.Geocode(reverseGeocodeResource, (ReverseGeocodeResponse response) => + { + if (response == null || response.Features == null || response.Features.Count == 0) + { + Debug.Log("No address found"); + return; + } + + var firstResult = response.Features[0]; + Debug.Log($"Address: {firstResult.PlaceName}"); + Debug.Log($"Type: {string.Join(", ", firstResult.PlaceType)}"); + }); + } +} +``` + +### What This Does +- Waits for the map to initialize +- Creates a reverse geocoding request for coordinates (37.8199, -122.4783) +- Returns the human-readable address or place name at that location +- The first feature typically contains the most specific address + +### Optional Parameters + +You can filter reverse geocoding results by type: + +```csharp +var reverseGeocodeResource = new ReverseGeocodeResource(location) +{ + Types = new[] { "address" } // Only return street addresses +}; +``` + +Common types: `country`, `region`, `postcode`, `place`, `locality`, `neighborhood`, `address`, `poi` + +### Response Data + +The `ReverseGeocodeResponse` contains: +- `Features` - List of places at or near the coordinates +- `Query` - The original coordinates as [longitude, latitude] +- `Attribution` - Mapbox attribution text + +Each `Feature` includes the same fields as forward geocoding (see above). + +--- + +## Common Use Cases + +### Interactive Map Click +Convert clicked map positions to addresses: +```csharp +// Get world position from mouse click, convert to lat/lng, then reverse geocode +var worldPos = GetClickedWorldPosition(); +var latLng = mapInformation.ConvertPositionToLatLng(worldPos); +var reverseResource = new ReverseGeocodeResource(latLng); +geocodingApi.Geocode(reverseResource, HandleReverseGeocodeResponse); +``` + +### Search Box +Implement a location search: +```csharp +var forwardResource = new ForwardGeocodeResource(searchText) +{ + Autocomplete = true, + Country = new[] { "us" } +}; +geocodingApi.Geocode(forwardResource, DisplaySearchResults); +``` + +--- diff --git a/Documentation~/UsingMapMatchingApi.md b/Documentation~/UsingMapMatchingApi.md new file mode 100644 index 000000000..bc1707077 --- /dev/null +++ b/Documentation~/UsingMapMatchingApi.md @@ -0,0 +1,245 @@ + +# Using the Mapbox Map Matching API + +The Mapbox Map Matching API snaps GPS traces (sequences of coordinates) to the road network and returns the matched route. This is useful for: +- Cleaning up noisy GPS traces +- Matching recorded GPS tracks to actual roads +- Analyzing vehicle or pedestrian movement patterns +- Creating accurate route visualizations from tracking data + +For complete API reference and details, see the [official Mapbox Map Matching API documentation](https://docs.mapbox.com/api/navigation/map-matching/). + +## What is Map Matching? + +Map matching takes a series of coordinates (typically from GPS tracking) and "snaps" them to the nearest roads, producing a clean, road-network-constrained path. Unlike the Directions API which calculates an optimal route between points, Map Matching respects the actual path taken and corrects for GPS inaccuracies. + +## Basic Usage + +```csharp +using UnityEngine; +using Mapbox.Unity.Map; +using Mapbox.DirectionsApi.MapMatching; +using Mapbox.BaseModule.Data.Vector2d; + +public class MapMatchingExample : MonoBehaviour +{ + public MapBehaviourCore MapCore; + private MapboxMapMatcherApi _mapMatcherApi; + + void Start() + { + MapCore.Initialized += map => + { + _mapMatcherApi = new MapboxMapMatcherApi(map.MapService.FileSource, 30); + MatchGPSTrace(); + }; + } + + void MatchGPSTrace() + { + // Example: noisy GPS coordinates from a recorded trip + var gpsTrace = new Vector2d[] + { + new Vector2d(37.7749, -122.4194), + new Vector2d(37.7751, -122.4193), + new Vector2d(37.7753, -122.4191), + new Vector2d(37.7755, -122.4189), + new Vector2d(37.7757, -122.4187) + }; + + // Create map matching request + var mapMatchingResource = new MapMatchingResource + { + Coordinates = gpsTrace, + Profile = Profile.MapboxDriving + }; + + // Query the Map Matching API + _mapMatcherApi.Match(mapMatchingResource, (MapMatchingResponse response) => + { + if (response.HasMatchingError) + { + Debug.LogError($"Map matching error: {response.MatchingError}"); + return; + } + + if (response.Matchings == null || response.Matchings.Length == 0) + { + Debug.Log("No matches found"); + return; + } + + var match = response.Matchings[0]; + Debug.Log($"Match confidence: {match.Confidence:F2}"); + Debug.Log($"Distance: {match.Distance} meters"); + Debug.Log($"Duration: {match.Duration} seconds"); + Debug.Log($"Matched geometry points: {match.Geometry.Count}"); + }); + } +} +``` + +## What This Does +- Takes a sequence of GPS coordinates (simulating a tracked route) +- Sends them to the Map Matching API +- Returns a cleaned, road-snapped version of the route +- Includes a confidence score indicating how well the trace matched to roads + +## Required Parameters + +### Coordinates +An array of 2-100 coordinates representing the GPS trace: +```csharp +mapMatchingResource.Coordinates = new Vector2d[] +{ + new Vector2d(lat1, lng1), + new Vector2d(lat2, lng2), + // ... more points +}; +``` + +### Profile +The routing profile to use for matching: +```csharp +Profile.MapboxDriving // Match to driving roads +Profile.MapboxDrivingTraffic // Match with traffic considerations +Profile.MapboxWalking // Match to pedestrian paths +Profile.MapboxCycling // Match to cycling routes +``` + +## Optional Parameters + +### Radiuses +Specify the GPS accuracy for each coordinate (in meters). Higher values for noisy traces: +```csharp +mapMatchingResource.Radiuses = new uint[] { 10, 10, 15, 20, 10 }; +// Values between 1-30: lower (1-10) for clean traces, higher (20-30) for noisy +``` + +### Timestamps +Unix timestamps for each coordinate (useful for speed calculations): +```csharp +long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); +mapMatchingResource.Timestamps = new long[] +{ + now, + now + 5, // 5 seconds later + now + 10, // 10 seconds later + now + 15, + now + 20 +}; +``` + +### Steps +Get turn-by-turn instructions: +```csharp +mapMatchingResource.Steps = true; +``` + +### Overview +Control the geometry detail level: +```csharp +mapMatchingResource.Overview = Overview.Full; // Most detailed +mapMatchingResource.Overview = Overview.Simplified; // Simplified (default) +mapMatchingResource.Overview = Overview.None; // No geometry +``` + +### Tidy +Remove GPS trace clusters and re-sample for better matching: +```csharp +mapMatchingResource.Tidy = true; +``` + +### Annotations +Request additional metadata: +```csharp +mapMatchingResource.Annotations = Annotations.Duration | Annotations.Distance | Annotations.Speed; +``` + +### Geometries +Specify the geometry format: +```csharp +mapMatchingResource.Geometries = Geometries.Polyline; // Default, precision 5 +mapMatchingResource.Geometries = Geometries.Polyline6; // Precision 6 +mapMatchingResource.Geometries = Geometries.GeoJson; // GeoJSON format +``` + +## Response Data + +The `MapMatchingResponse` contains: + +### Error Checking +```csharp +if (response.HasMatchingError) +{ + Debug.LogError(response.MatchingError); + return; +} +``` + +### Matchings Array +Each `MatchObject` includes: +- `Confidence` - Float between 0 (low) and 1 (high) indicating match quality +- `Distance` - Total matched route distance in meters +- `Duration` - Estimated travel time in seconds +- `Geometry` - The road-snapped route as Vector2d points +- `Weight` - Route weight (duration-based) +- `Legs` - Individual segments if multiple waypoints + +### Tracepoints Array +Information about each input coordinate: +- `Location` - The snapped location on the road network +- `Name` - Name of the matched street/road +- `WaypointIndex` - Index in the matched route +- `MatchingsIndex` - Which matching this belongs to +- `AlternativesCount` - Number of alternative matches (0 = unambiguous match) + +## Common Use Cases + +### Clean GPS Tracking Data +```csharp +// Convert noisy GPS logs to clean routes +var gpsLog = LoadGPSLog(); // Your GPS coordinates +var matchRequest = new MapMatchingResource +{ + Coordinates = gpsLog, + Profile = Profile.MapboxDriving, + Tidy = true, // Remove clusters + Radiuses = GetRadiuses(gpsLog) // Set based on GPS accuracy +}; +``` + +### Analyze Traveled Routes +```csharp +// Get detailed information about a traveled route +var matchRequest = new MapMatchingResource +{ + Coordinates = recordedPath, + Profile = Profile.MapboxDriving, + Annotations = Annotations.Duration | Annotations.Distance | Annotations.Speed, + Steps = true // Get turn-by-turn of actual path taken +}; +``` + +### Visualize Movement Patterns +```csharp +// Match and visualize user movement +var matchRequest = new MapMatchingResource +{ + Coordinates = userPath, + Profile = Profile.MapboxWalking, + Overview = Overview.Full // Get full geometry for drawing +}; +``` + +## Differences from Directions API + +| Feature | Directions API | Map Matching API | +|---------|---------------|------------------| +| Purpose | Find optimal route | Match GPS trace to roads | +| Input | 2-25 waypoints | 2-100 coordinates | +| Output | Optimal calculated route | Road-snapped actual path | +| Use Case | Navigation | GPS trace analysis | +| Path Adherence | Optimized | Follows input path | + +--- diff --git a/Editor/Mapbox.UnitySDK.Editor.asmdef b/Editor/Mapbox.UnitySDK.Editor.asmdef index 313e9198f..1ca996427 100644 --- a/Editor/Mapbox.UnitySDK.Editor.asmdef +++ b/Editor/Mapbox.UnitySDK.Editor.asmdef @@ -2,7 +2,8 @@ "name": "MapboxUnitySDK.Editor", "rootNamespace": "", "references": [ - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:36ca6af6e2b304d4090888554d6ce199" ], "includePlatforms": [ "Editor" diff --git a/Editor/MapboxConfigurationWindow.cs b/Editor/MapboxConfigurationWindow.cs index de8d3a5a2..2308d3b4a 100644 --- a/Editor/MapboxConfigurationWindow.cs +++ b/Editor/MapboxConfigurationWindow.cs @@ -3,6 +3,7 @@ using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; +using Mapbox.UnityMapService; using UnityEditor; using UnityEngine; @@ -104,8 +105,8 @@ private static void WriteConfigFile(MapboxConfiguration config, string path) static void OpenWindow() { _mapboxContext = new MapboxContext(); + _mapboxContext.LoadConfigurationCoroutine(false).MoveNext(); EditorApplication.delayCall -= OpenWindow; - //instantiate the config window instance = GetWindow(typeof(MapboxConfigurationWindow)) as MapboxConfigurationWindow; instance.minSize = new Vector2(800, 180); instance.titleContent = new GUIContent("Mapbox Setup"); @@ -324,7 +325,20 @@ public class MapboxMenu [MenuItem("Mapbox/Clear Caches")] public static void DeleteAllCache() { - MapboxCacheManager.DeleteAllCache(); + if (Application.isPlaying) + { + var core = GameObject.FindObjectOfType(); + if (core == null) + { + Debug.LogError("No Map Core found"); + return; + } + core.MapboxMap.ClearCachedData(); + } + else + { + MapboxCacheManager.DeleteAllCache(); + } } } } \ No newline at end of file diff --git a/README.md b/README.md index 53a0dc296..3c1664dbd 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,225 @@ -##Mapbox Unity SDK 3.0 Quickstart guide +# Mapbox Unity SDK -### Installing the Package +Map rendering, location services, directions, and geocoding for Unity. Distributed as a Unity Package Manager (UPM) package. -1. **Get the Mapbox Unity SDK v3.0** - - Clone the Mapbox Unity SDK v3.0 branch to local. +| | | +| --- | --- | +| **Package name** | `com.mapbox.sdk` | +| **Current version** | `3.1.0` | +| **Vendor** | Mapbox — | +| **Minimum Unity** | `2022.3` LTS | +| **Build targets** | iOS, Android | +| **Render pipeline** | Universal Render Pipeline (URP) — required | -2. **Open Unity Package Manager (UPM)** - - Open your Unity project and navigate to the Unity Package Manager. +For release notes see [`CHANGELOG.md`](CHANGELOG.md). Upgrading from v3.0? See [`Documentation~/Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md). -3. **Add the Package from Disk** - - Click the **`Add package from disk`** button in the top-left corner of the UPM window. - - Locate and select the `package.json` file within the Mapbox folder. +--- + +## Supported Unity editor versions + +| Editor version | Status | +| --- | --- | +| **2022.3 LTS** | Primary target. Android build-settings folder shipped at `Runtime/AndroidBuildSettings/2022.3.38f1/`. | +| **2021.3 LTS** | Best-effort. Android build-settings folder shipped at `Runtime/AndroidBuildSettings/2021.3.41f1/`. | +| **6000.x (Unity 6)** | Best-effort. Android build-settings folder shipped at `Runtime/AndroidBuildSettings/6000/`. | +| **2023.x non-LTS** | Untested. | +| **< 2021.3** | Unsupported. | + +When building for Android, copy the `Plugins/` folder from the matching (or closest lower) editor version's subfolder under `Runtime/AndroidBuildSettings/` into your project's `Assets/`. See [Building for Android](#building-for-android) for the full procedure. + +## Supported build platforms + +Only **iOS** and **Android** are supported build targets. Use the Unity Editor (Windows or macOS) for development and Play-mode testing. Standalone, WebGL, console, and VisionOS are not supported. -4. **Verify Installation** - - The Mapbox Unity SDK package should now appear in the Unity Package Manager window. +| Platform | Status | Notes | +| --- | --- | --- | +| **iOS** | Supported | XCFrameworks under `Runtime/Mapbox/BaseModule/Plugins/iOS/`. ARM64. IL2CPP. | +| **Android** | Supported | Native libs under `Runtime/Mapbox/BaseModule/Plugins/sqlite/Android/libs/` for `arm64-v8a`, `armeabi-v7a`, `x86`. Project-side configuration required — see [Building for Android](#building-for-android). | +| **Unity Editor (Play mode)** | Supported on Windows / macOS for development. | Not a shipping target. | +| **All other platforms** | Not supported. | Standalone, WebGL, console, VisionOS, etc. | + +## Dependencies + +Resolved automatically by UPM: + +| Package | Version | Required | Used for | +| --- | --- | --- | --- | +| `com.unity.render-pipelines.universal` | `10.10.1` | Yes | Terrain and building shaders ship as URP Shader Graphs. | +| `com.unity.nuget.newtonsoft-json` | `3.2.1` | Yes | JSON parsing for tile metadata and API responses. | +| `com.unity.burst` | `1.8.12` | Yes | Burst-compiled terrain decode and collider job. First-time domain reload pays a one-shot AOT compile. | +| `com.unity.inputsystem` | `1.7.0` | Yes | Camera input backend selection is controlled by Player Settings → Active Input Handling, not by this dependency; "Old" (default) keeps legacy `UnityEngine.Input` behavior. See `Documentation~/CameraSystem.md`. | --- -### Playing the Demo Scenes +## Installing the package -1. **Access the Demo Scenes** - - The SDK includes two demo scenes: - - `Location Based Game` - - `World Map Viewer` +1. **Clone or download the SDK.** This package is distributed from the SDK repository. +2. **Open Unity Package Manager** in your Unity project (`Window → Package Manager`). +3. **Add the package from disk.** Click the `+` in the top-left, choose `Add package from disk…`, and select the SDK's `package.json`. +4. **Verify.** The Mapbox Unity SDK should now appear in the list of installed packages. -2. **Import the Demo Scenes** - - In the Package Manager, select the Mapbox Unity SDK package. - - Go to the "Samples" tab. - - Hit the **Import** button to create a `Samples` folder at the root of your project. - - Unity will copy the demo scene files into this folder. +> Editor-only evaluation? You can stop here. iOS and Android each have one extra step at build time — see [Building for iOS](#building-for-ios) and [Building for Android](#building-for-android) when you're ready to ship. + +--- -3. **Configure Your Access Token** - - The demo scenes will not work until you set your access token. - - Open the **Token Configuration** window under the Mapbox section in Unity's top menu. - - Enter your access token. +## Access token -4. **Token Storage** - - Unity will save the access token in a file located at: - `Resources/Mapbox/MapboxConfiguration.txt` +Every Mapbox API call (tiles, geocoding, directions) requires a Mapbox access token. -5. **Run the Demo Scenes** - - After configuring your access token, open the demo scenes and play them. +- **Set it** via the editor menu: **Mapbox → Setup**. +- **Stored** at `Assets/Resources/Mapbox/MapboxConfiguration.txt`, loaded at runtime via `Resources.Load`. +- An SKU token is appended to signed requests automatically. +- **Get a token** at . --- -### Integrating the Location-Based Game Demo Scene into Your Project +## Demo scenes -The integration process depends heavily on your project setup and how you intend to use the map. Below are steps to help you get started with a location-based game setup: +Five samples ship with the SDK. To import: open the Package Manager, select the Mapbox Unity SDK, go to the **Samples** tab, and click **Import** next to the one you want. -1. **Import the Location-Based Game Sample** - - Navigate to `Samples/Mapbox Unity SDK/3.0.0/Location Based Game`. +| Sample | What it demonstrates | +| --- | --- | +| **WorldMapViewer** | Minimal map setup — pan / zoom an interactive world map. Best first-time evaluation target. | +| **LocationBasedGame** | Device GPS → in-scene avatar movement. The Pokemon-Go-style starting point. | +| **MapboxComponents** | High-performance component-system-based layer rendering (Buildings + Areas + Roads). | +| **DirectionsApiDemo** | Calling the Directions API + rendering a route on the map. | +| **GeocodingApiDemo** | Forward and reverse geocoding. | -2. **Use the `LocationBasedMap` Prefab** - - Add the `LocationBasedMap` prefab to any scene, and it should work. - - Optionally, copy the following to another location in your project for easier customization: - - `LocationBasedMap` prefab - - `MapVisuals` folder - - `Scripts` folder - - After copying, you can safely delete the `Samples` folder if it’s no longer needed. +After import each sample lives under `Assets/Samples/Mapbox Unity SDK///`. You can copy the relevant pieces into your own project layout and delete the `Samples` folder. + +The **LocationBasedGame** sample includes a `LocationBasedMap` prefab that's a useful starting point for any GPS-driven game: drop it into a scene and you have a working location-aware map with a follow-camera and a character that follows the device location. --- -### Working with Location +## Working with location -The Mapbox Unity SDK provides two primary methods for working with location: +Two ways to drive the map's center / orientation: -##### 1. Latitude and Longitude Field in the Map Script +### 1. Fixed lat/lon from the Inspector -- The **Mapbox Map Behaviour** script includes a field for latitude and longitude. -- If the `Initialize On Start` checkbox is enabled: - - The specified latitude and longitude will serve as the starting location of the map. - - This method is simple and effective, particularly during development in the Unity editor. +`MapboxMapBehaviour` has a `MapInformation` field with editable latitude, longitude, zoom, pitch, bearing, and scale. With `Initialize On Start` checked, the map opens at those coordinates at startup. -**Limitations**: -- This approach does not use the device's location. -- If you build the project with these settings, the map will display the predefined location specified in the script. +Good for development and for apps where the map's location is fixed or driven by your own UI (a city tour, a real-estate app filtered by area, etc.). -**Usage in the Location-Based Game Demo Scene**: -- You can move the `CharTarget` game object within the scene. The Astronaut character will walk toward this object in Unity space. +### 2. Device GPS via the `LocationModule` +The `LocationModule` system swaps in device GPS at build time and predefined test coordinates in the editor. This is what the `LocationBasedGame` sample uses. -##### 2. Using the LocationModule System +To wire it up on a new map: -For a more advanced and dynamic solution, you can use the **LocationModule** system. This method is used by default in the Location-Based Game demo scene. +1. **Disable `Initialize On Start`** on `MapboxMapBehaviour` — the location provider will initialize the map asynchronously when the first GPS fix comes in. +2. **Add `Snap Map To Location Provider`** to your map GameObject. It will move the map center to follow the device location. +3. **Attach `Snap Transform To Location Provider`** to your character / avatar transform to make it follow the device location in world space. -###### Overview: -- The LocationModule system abstracts location handling: - - In builds: Uses the device's location. - - In the editor: Uses predefined location values. -- Predefined values can be configured in the **LocationModule** game object located under the map prefab. +For arbitrary placement (drop a marker at a specific lat/lon, not driven by GPS), see [Place a GameObject at a latitude / longitude](Documentation~/GettingStartedWithMapboxMapObject.md#place-a-gameobject-at-a-latitude--longitude) in the Getting Started guide. -###### Steps to Use the LocationModule System: -Step 1. **Disable `Initialize On Start`** - - Uncheck the `Initialize On Start` checkbox in the map script. +--- -Step 2. **Add `Snap Map To Location Provider`** - - Attach the `Snap Map To Location Provider` script to your map. - - Set it up as shown in the provided documentation or image reference. +## Building for iOS -Step 3. **Enable Player Movement with Device Location** - - Attach the `Snap Transform To Location Provider` script to the **`CharTarget`** game object. - - Configure the script to update the position of the `CharTarget` object with the device's location in real-time. - - The player avatar will automatically follow the `CharTarget` object as it moves. +| Setting | Value | +| --- | --- | +| **Scripting Backend** | IL2CPP (required) | +| **Minimum iOS Version** | 11.0+ recommended | +| **Architecture** | ARM64 | +| **Location permission** | Set `NSLocationWhenInUseUsageDescription` (or `NSLocationAlwaysUsageDescription`) in Info.plist before requesting location. | -By following these steps, your map will dynamically update based on the device's location, and the player avatar will respond to real-world movements. +XCFrameworks (`MapboxCommon`, `Turf`) ship multi-slice and are picked up automatically by Unity's plugin importer. No further project-side configuration required. --- -### Building Your Map Application for Android +## Building for Android + +### Step 1 — Copy the build-settings plugin folder + +The SDK ships per-editor-version Gradle templates and manifest entries that need to live in your project's `Assets/` folder. + +1. Open `Runtime/AndroidBuildSettings/`. There are subfolders named after Unity editor versions. +2. Pick the one matching (or closest lower than) the Unity version you're using. +3. Copy the `Plugins/` folder inside it into your project's `Assets/`. +4. If your project already has custom Android settings, merge them with the copied files rather than overwriting. + +Verify the settings landed: in **Project Settings → Player → Publish Settings**, the custom files should be referenced. + +### Step 2 — Managed Stripping Level + +In Player Settings → Android → Other Settings → Optimization → **Managed Stripping Level**: + +- **Disabled / Minimal / Low** — the primary tested target. Recommended for shipping. +- **Medium** — briefly tested in v3.1.0 and appears to work. The SQLite cache's model classes carry `[UnityEngine.Scripting.Preserve]` plus a backup `Plugins/link.xml` declaration so reflection-based row mapping survives stripping. We haven't exercised every code path under Medium; report anything that misbehaves. +- **High** — not tested. Likely works given the `[Preserve]` coverage, but no guarantees. If you need High in production, validate on your specific build before committing. + +If you observe `Error inserting … (extended=ConstraintForeignKey)` in logcat at any stripping level, drop to **Minimal / Low** as a workaround and file an issue — the preserve-attribute combination should have prevented it. -To build your map application for the Android platform, you'll need specific settings files included in the Mapbox Unity SDK package. Follow these steps: +### Step 3 — Location permissions -##### Step 1: Copy Required Files -- Go to the `Mapbox Unity SDK/Runtime/AndroidBuildSettings` folder, you'll find a few folders named after Unity Editor Version. -- Find the one matching (or closest lower) with the version you are using -- Copy the `Plugins` folder inside to your project. Place it under the `Assets` folder. +For GPS, your manifest should request: -###### Step 1b: Merging Custom Android Settings (if applicable) -- If your project already contains custom Android settings, merge them with the copied files to ensure compatibility. +- `android.permission.ACCESS_FINE_LOCATION` +- `android.permission.ACCESS_COARSE_LOCATION` -##### Step 2: Verify Player Settings -- Open **Project Settings** and navigate to the **Player** section. -- Under the **Publish Settings** section (at the bottom), verify that the custom setting files we just copied are applied. +Runtime permission prompts are handled by the SDK's `UniAndroidPermission` helper — no project-side code required. + +### Step 4 — Build + +After the steps above, Build & Run from Unity's Build Settings as normal. + +--- + +## Modules + +The SDK is organized into module assemblies. Each is a separate `.asmdef` and can be referenced or excluded independently: + +| Assembly | Path | Responsibility | +| --- | --- | --- | +| `MapboxBaseModule` | `Runtime/Mapbox/BaseModule/` | Tile management, caching (memory/file/SQLite), data fetching, coordinate conversions. Foundation for every other module. | +| `UnityMapModule` | `Runtime/Mapbox/UnityMapService/` | Unity-specific map service (`MapUnityService`), quadtree tile provider. | +| `MapboxLocationModule` | `Runtime/Mapbox/LocationModule/` | GPS / compass via `AbstractLocationProvider`. Unity + static (editor) implementations. | +| `MapboxImageModule` | `Runtime/Mapbox/ImageModule/` | Raster tile imagery (satellite, streets). Terrain strategies (flat / elevated). | +| `MapboxVectorModule` | `Runtime/Mapbox/VectorModule/` | Vector tile rendering — buildings, roads, areas. Modifier-stack mesh generation + Component System. | +| `MapboxCustomImageryModule` | `Runtime/Mapbox/CustomImageryModule/` | Custom tileset support (non-Mapbox sources via TMS or arbitrary URL templates). | +| `MapboxDirections` | `Runtime/Mapbox/DirectionsApi/` | Directions API wrapper. | +| `MapboxGeocoding` | `Runtime/Mapbox/GeocodingApi/` | Forward / reverse geocoding API wrapper. | +| `MapboxDebug` | `Runtime/Mapbox/MapDebug/` | Debug logging and benchmarking pipelines. | +| `MapboxExamples` | `Runtime/Mapbox/Example/` | Camera behaviours, input handling, demo support scripts. | + +Editor-only counterparts exist for several modules (e.g. `MapboxBaseModule.Editor`). + +--- + +## Documentation + +In-package documentation lives under `Documentation~/`. Grouped by what you're trying to do: + +**Upgrading from a previous version** +- [`Migration-3.0-to-3.1.md`](Documentation~/Migration-3.0-to-3.1.md) — step-by-step upgrade from v3.0 to v3.1. Read this first if you're upgrading. + +**Getting started** +- [`GettingStartedWithMapboxMapObject.md`](Documentation~/GettingStartedWithMapboxMapObject.md) — first-time setup, the `MapboxMap` object, and a place-a-pin-at-lat/lon recipe. +- [`AccessingTheMapObject.md`](Documentation~/AccessingTheMapObject.md) — getting a reference to the map at runtime. +- [`WorkingWithMapObject.md`](Documentation~/WorkingWithMapObject.md) — runtime API. + +**Map composition** +- [`WorkingWithModules.md`](Documentation~/WorkingWithModules.md) — module composition pattern, architecture diagrams. +- [`ChangingMapLocation.md`](Documentation~/ChangingMapLocation.md) — moving the map to a new lat/lon. +- [`ChangeImageryStyleOnRuntime.md`](Documentation~/ChangeImageryStyleOnRuntime.md) — switching tile styles at runtime. +- [`CoordinateConversions.md`](Documentation~/CoordinateConversions.md) — lat/lon ↔ Mercator ↔ Unity world space. + +**Camera, input, and AR/MR** +- [`CameraSystem.md`](Documentation~/CameraSystem.md) — camera behaviours, touch / mouse input, Input System soft-dep, AR/MR parent-transform guidance. + +**Vector content** +- [`VectorLayerModule.md`](Documentation~/VectorLayerModule.md) — building / road / area visualizers, modifier stacks. +- [`ComponentsModule.md`](Documentation~/ComponentsModule.md) — high-performance component-based vector pipeline. +- [`WorkingWithPois.md`](Documentation~/WorkingWithPois.md) — point-of-interest features. + +**APIs** +- [`UsingDirectionsApi.md`](Documentation~/UsingDirectionsApi.md) — Directions API wrapper. +- [`UsingGeocodingApi.md`](Documentation~/UsingGeocodingApi.md) — Geocoding API wrapper. +- [`UsingMapMatchingApi.md`](Documentation~/UsingMapMatchingApi.md) — Map Matching API wrapper. + +--- -##### Step 3: Build Your Application -- After completing the above steps, build your application for the Android platform. +## Reporting issues / contributing +This package is maintained by Mapbox internally. Issues and feature requests should be reported through your standard Mapbox support channel. diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs new file mode 100644 index 000000000..1a80ca5c8 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; + +namespace Mapbox.BaseModule.Data.DataFetchers +{ + /// + /// Size-keyed pool of float[] buffers used by terrain-RGB elevation extraction. + /// Cuts the ~256 KB allocation per data tile decode down to a one-time cost per + /// resolution: tiles that drop out of the memory cache return their backing array + /// here, and the next decode rents it back instead of allocating fresh. + /// + /// + /// Thread-safe — async GPU readback callbacks fire on the main thread but the contract + /// is conservative in case Unity changes that. Per-size stack depth is capped at + /// to bound worst-case RAM after a CacheSize spike; in + /// practice the only sizes seen are 256×256 (default texture) and + /// 512×512 (retina), so the cap also indirectly limits total residency. + /// + public static class ElevationArrayPool + { + // 32 × 1 MB (512×512 floats) = ~32 MB worst-case per resolution. Anything beyond + // is dropped to GC; tiles past this point pay a fresh allocation on next decode. + private const int MaxPerSizeDepth = 32; + + private static readonly Dictionary> _pools = new Dictionary>(); + + // Editor play-mode stop leaves static state untouched, so the pool would keep + // the previous session's buffers referenced. Reset on subsystem registration + // (fires before scene load on play-mode entry). +#if UNITY_EDITOR + [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetForPlayMode() + { + lock (_pools) + { + _pools.Clear(); + } + } +#endif + + /// + /// Returns a float[] of exactly entries, either from + /// the pool or freshly allocated. Contents are undefined; the caller is expected to + /// overwrite every index. + /// + public static float[] Rent(int size) + { + lock (_pools) + { + if (_pools.TryGetValue(size, out var stack) && stack.Count > 0) + { + return stack.Pop(); + } + } + return new float[size]; + } + + /// + /// Returns to the pool. No-op if null. The caller must not + /// hold any references to after calling. + /// + public static void Return(float[] array) + { + if (array == null) + { + return; + } + lock (_pools) + { + if (!_pools.TryGetValue(array.Length, out var stack)) + { + stack = new Stack(); + _pools[array.Length] = stack; + } + AssertNotAlreadyInStack(stack, array); + if (stack.Count >= MaxPerSizeDepth) + { + return; // Drop to GC; cap bounds peak RAM held by the pool. + } + stack.Push(array); + } + } + + // O(N) check that only runs in editor/dev builds. Double-return would silently + // alias two callers to the same backing buffer on the next pair of Rent() calls, + // producing data corruption that's hard to track down. + [System.Diagnostics.Conditional("UNITY_ASSERTIONS")] + private static void AssertNotAlreadyInStack(Stack stack, float[] array) + { + foreach (var existing in stack) + { + if (ReferenceEquals(existing, array)) + { + UnityEngine.Debug.LogError("ElevationArrayPool.Return called twice for the same array — likely double-return. The duplicate is being dropped."); + throw new System.InvalidOperationException("ElevationArrayPool double-return detected."); + } + } + } + } +} diff --git a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta similarity index 83% rename from Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta rename to Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta index 892db1002..00ded1549 100644 --- a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs.meta +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/ElevationArrayPool.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 46cd4a863e31244f891143a5bab077bc +guid: aabd60e0ea4fd4582ad15456847dcabe MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs index 881b1391d..a0955c3c0 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/MapboxTileData.cs @@ -14,16 +14,28 @@ public class MapboxTileData public bool HasError = false; [HideInInspector] public byte[] Data; + // Multicast: a single TerrainData instance is shared across up to 16 render tiles + // (data zoom = render zoom − 2), each of which needs its own cleanup callback on + // eviction. The prior single-callback Set/overwrite pattern silently dropped 15 of + // them. Subscribers attach with AddDisposeCallback and must symmetrically remove. private Action _onDispose; public virtual void Dispose() { _onDispose?.Invoke(); + _onDispose = null; } - internal void SetDisposeCallback(Action callback) + public void AddDisposeCallback(Action callback) { - _onDispose = callback; + if (callback == null) return; + _onDispose += callback; + } + + public void RemoveDisposeCallback(Action callback) + { + if (callback == null) return; + _onDispose -= callback; } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs index a4bb35f5b..e82b9d9a5 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/Source.cs @@ -90,6 +90,11 @@ public virtual void Cancel(CanonicalTileId tileId) } + public virtual IEnumerator ChangeTilesetId(string tilesetId) + { + yield break; // Exit immediately without waiting a frame + } + public virtual void OnDestroy() { diff --git a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs index 1d03a1f20..0bc8d94df 100644 --- a/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs +++ b/Runtime/Mapbox/BaseModule/Data/DataFetchers/TerrainData.cs @@ -9,31 +9,79 @@ public class TerrainData : RasterData { [HideInInspector] public float[] ElevationValues; public bool IsElevationDataReady = false; - public Action ElevationValuesUpdated; + + // Flipped to true by Dispose(). Async readback paths can complete after the + // owning TerrainData has been evicted; they check this flag before assigning + // and return the rented buffer to the pool instead of leaking it. + public bool IsDisposed { get; private set; } + + /// + /// Fires whenever or + /// completes. Multiple + /// subscribers can safely attach with += and detach with -=; the + /// former single-setter SetElevationChangedCallback method silently wiped + /// other subscribers, so it was removed. + /// + public event Action ElevationValuesUpdated; + public float MinElevation = 0; public float MaxElevation = 0; - public void SetElevationChangedCallback(Action callback) - { - ElevationValuesUpdated = callback; - } - + /// + /// True as soon as the raster has been assigned, + /// regardless of whether the per-pixel float[] has been decoded yet. Shader + /// elevation rendering only needs the texture and can show a tile the moment this + /// flips true; CPU consumers (collider builder, QueryElevation APIs, CPU elevation + /// mode) should wait for instead. + /// + public bool IsTextureReady => Texture != null; + public override void Clear() { base.Clear(); IsElevationDataReady = false; } + /// + /// Returns the rented array to + /// when the cache evicts this entry, then runs + /// the standard dispose callback. After this call + /// ElevationValues is null; nothing should hold a reference past dispose. + /// + public override void Dispose() + { + // Explicit idempotency guard. Today the ElevationValues null-check below + // also prevents a double Return-to-pool, but the AsyncExtract late-callback + // can reassign ElevationValues after Dispose (its IsDisposed check returns + // the buffer to the pool, but a subsequent second Dispose without this guard + // would still see IsElevationDataReady etc. flicker). Cheap defensive bail. + if (IsDisposed) return; + if (ElevationValues != null) + { + ElevationArrayPool.Return(ElevationValues); + ElevationValues = null; + } + IsElevationDataReady = false; + IsDisposed = true; + base.Dispose(); + } + + // Cached side length of the square heightmap (sqrt(Length)). Computed once + // on Set to avoid per-call Mathf.Sqrt in QueryHeightData / ReadElevation. + private int _cachedWidth; + public void SetElevationValues(float[] elevationArray) { ElevationValues = elevationArray; + _cachedWidth = elevationArray != null ? (int)Mathf.Sqrt(elevationArray.Length) : 0; IsElevationDataReady = true; ElevationValuesUpdated?.Invoke(); } - + public void SetElevationValues(float[] elevationArray, float min, float max) { ElevationValues = elevationArray; + _cachedWidth = elevationArray != null ? (int)Mathf.Sqrt(elevationArray.Length) : 0; IsElevationDataReady = true; MinElevation = min; MaxElevation = max; @@ -52,33 +100,48 @@ public float QueryHeightData(CanonicalTileId requestingSubTileId, float x, float public float QueryHeightData(Vector2 point) { + // Mirror the 3-arg overload's guard: shader-only mode (ExtractCpuElevationData=false) + // leaves ElevationValues null and _cachedWidth=0 — ReadElevation would index a + // null array with sectionWidth=-1 and NRE. + if (!(ElevationValues?.Length > 0)) return 0; return ReadElevation(point.x, point.y, new Vector4(1, 1, 0, 0)); } - + public float QueryHeightData(float x, float y) { + if (!(ElevationValues?.Length > 0)) return 0; return ReadElevation(x, y, new Vector4(1, 1, 0, 0)); } private float ReadElevation(float x, float y, Vector4 terrainTextureScaleOffset) { - var width = (int) Mathf.Sqrt(ElevationValues.Length); - var sectionWidth = width * terrainTextureScaleOffset.x - 1; - var padding = width * new Vector2(terrainTextureScaleOffset.z, terrainTextureScaleOffset.w); + var width = _cachedWidth; + var sectionWidth = width * terrainTextureScaleOffset.x - 1f; + var xx = terrainTextureScaleOffset.z * width + Mathf.Clamp01(x) * sectionWidth; + var yy = terrainTextureScaleOffset.w * width + Mathf.Clamp01(y) * sectionWidth; - var xx = padding.x + (x * sectionWidth); - var yy = padding.y + (y * sectionWidth); + // Bilinear sample — when the caller's UV resolution is finer than the data + // texture's sub-region (typical for render tiles sharing one data tile with 15 + // neighbors), nearest-neighbor sampling produces visible stepping. + if (xx < 0f) xx = 0f; else if (xx > width - 1) xx = width - 1; + if (yy < 0f) yy = 0f; else if (yy > width - 1) yy = width - 1; - var index = (int) yy * width - + (int) xx; - if (ElevationValues.Length <= index) - { - return 0; - } - else - { - return ElevationValues[(int) yy * width + (int) xx]; - } + var x0 = (int)xx; + var y0 = (int)yy; + var x1 = x0 + 1; if (x1 >= width) x1 = width - 1; + var y1 = y0 + 1; if (y1 >= width) y1 = width - 1; + var fx = xx - x0; + var fy = yy - y0; + + var row0 = y0 * width; + var row1 = y1 * width; + var h00 = ElevationValues[row0 + x0]; + var h10 = ElevationValues[row0 + x1]; + var h01 = ElevationValues[row1 + x0]; + var h11 = ElevationValues[row1 + x1]; + var h0 = h00 + (h10 - h00) * fx; + var h1 = h01 + (h11 - h01) * fx; + return h0 + (h1 - h0) * fy; } diff --git a/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs b/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs index ef892eb4c..032a06ae1 100644 --- a/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs +++ b/Runtime/Mapbox/BaseModule/Data/Interfaces/ITerrainLayerModule.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections; +using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Tiles; namespace Mapbox.BaseModule.Data.Interfaces @@ -5,5 +8,6 @@ namespace Mapbox.BaseModule.Data.Interfaces public interface ITerrainLayerModule : ILayerModule { float QueryElevation(CanonicalTileId tileId, float x, float y); + IEnumerator LoadTileData(CanonicalTileId tileId, Action callback = null); } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs index dfb43393e..c0578a6f9 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/FileCache.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Tasks; using Mapbox.BaseModule.Data.Tiles; @@ -85,10 +86,7 @@ public bool TestAvailability() } } - private string TileToRelativeFilePath(CanonicalTileId tileId, string tilesetId) - { - return string.Format("{0}/{1}{2}{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); - } + public virtual bool Exists(CanonicalTileId tileId, string mapId) { @@ -311,27 +309,45 @@ public InfoWrapper(MapboxTileData textureCacheItem, string path, Action } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string TileToRelativeFilePath(CanonicalTileId tileId, string tilesetId) + { + return string.Format("{0}/{1}_{2}_{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string TileToToFileInfoExpects(CanonicalTileId tileId, string tilesetId) + { + var relativePath = string.Format("{0}/{1}_{2}_{3}.{4}", MapIdToFolderName(tilesetId), tileId.X, tileId.Y, tileId.Z, FileExtension); + return Path.GetFullPath(Path.Combine(PersistantCacheRootFolderPath, relativePath)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private string TileToRelativePath(MapboxTileData cacheItem) { return TileToRelativeFilePath(cacheItem.TileId, cacheItem.TilesetId); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string TileToPathFileInfoExpects(MapboxTileData cacheItem) { return RelativeFilePathToFileInfoExpects(TileToRelativeFilePath(cacheItem.TileId, cacheItem.TilesetId)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string TileToPathFileInfoExpects(CanonicalTileId tileId, string tilesetId) { return RelativeFilePathToFileInfoExpects(TileToRelativeFilePath(tileId, tilesetId)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string RelativeFilePathToFileInfoExpects(string relativeFilePath) { var fullPath = Path.GetFullPath(Path.Combine(PersistantCacheRootFolderPath, relativeFilePath)); return fullPath; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public string RelativePathToUnityRequestExpects(string relativeFilePath) { var fullPath = Path.GetFullPath(Path.Combine(PersistantCacheRootFolderPath, relativeFilePath)); @@ -340,6 +356,7 @@ public string RelativePathToUnityRequestExpects(string relativeFilePath) return fullPath.Insert(0, "file://"); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private string FullFilePathToRelativePath(string fileInfoFullName) { return fileInfoFullName.Substring(PersistantCacheRootFolderPath.Length, diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs index 59d746331..be81d79cb 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/ISqliteCache.cs @@ -11,6 +11,7 @@ namespace Mapbox.BaseModule.Data.Platform.Cache public interface ISqliteCache { event Action DataPrunedForFile; + event Action DatabaseCleared; void ReadySqliteDatabase(); bool IsUpToDate(); diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs index 42695b9f3..dec8cbf56 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/MapboxCacheManager.cs @@ -50,6 +50,7 @@ public MapboxCacheManager(UnityContext unityContext, MemoryCache memoryCache, Fi if (_sqLiteCache != null && _textureFileCache != null) { _sqLiteCache.DataPrunedForFile += path => _textureFileCache.DeleteByFileRelativePath(path); + _sqLiteCache.DatabaseCleared += _textureFileCache.ClearAll; } if (_sqLiteCache != null) @@ -69,7 +70,11 @@ public MapboxCacheManager(UnityContext unityContext, MemoryCache memoryCache, Fi } } - + public void ClearCachedData() + { + _sqLiteCache.ClearDatabase(); + _sqLiteCache.ReadySqliteDatabase(); + } public virtual void SaveImage(RasterData textureCacheItem, bool forceInsert) { diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs index 51114a9c6..ceae022f1 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/SqliteCache.cs @@ -16,6 +16,7 @@ namespace Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache public class SqliteCache : ISqliteCache, IDisposable { public event Action DataPrunedForFile = s => { }; + public event Action DatabaseCleared = () => { }; public const int PruneCacheDelta = 20; @@ -227,7 +228,8 @@ public void SyncAdd(string tilesetName, CanonicalTileId tileId, byte[] data, str } catch (Exception ex) { - Debug.LogErrorFormat("Error inserting {0} {1} {2} ", tilesetName, tileId, ex); + var extended = (ex is SQLiteException) ? SQLite3.ExtendedErrCode(_sqlite.Handle).ToString() : "n/a"; + Debug.LogErrorFormat("Error inserting {0} {1} (extended={2}) {3} ", tilesetName, tileId, extended, ex); } // update counter only when new tile gets inserted @@ -379,6 +381,7 @@ public bool ClearDatabase() return false; } + DatabaseCleared(); return true; } @@ -415,6 +418,7 @@ public bool DeleteSqliteFile() Debug.LogError(error); } + DatabaseCleared(); return isDeletedSuccesfully; } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs index d6dbc6091..87d964127 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tiles.cs @@ -1,60 +1,78 @@ using System; using Mapbox.BaseModule.Data.Platform.SQLite; +using UnityEngine.Scripting; namespace Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache { /// - /// Don't change the class name: sqlite-net uses it for table creation + /// Don't change the class name: sqlite-net uses it for table creation. + /// [Preserve] forces IL2CPP managed-code-stripping to keep the property + /// accessors that sqlite-net invokes via reflection. /// + [Preserve] public class tiles { - [PrimaryKey, AutoIncrement] + [PrimaryKey, AutoIncrement, Preserve] public int id { get; set; } - + + [Preserve] public int tile_set { get; set; } //hrmpf: multiple PKs not supported by sqlite.net //https://github.com/praeclarum/sqlite-net/issues/282 //TODO: do it via plain SQL //[PrimaryKey] + [Preserve] public int zoom_level { get; set; } //[PrimaryKey] + [Preserve] public long tile_column { get; set; } //[PrimaryKey] + [Preserve] public long tile_row { get; set; } + [Preserve] public byte[] tile_data { get; set; } + [Preserve] public string tile_path { get; set; } - + /// Unix epoch for simple FIFO pruning + [Preserve] public int timestamp { get; set; } /// ETag Header value of the reponse for auto updating cache + [Preserve] public string etag { get; set; } /// Expiration date of cached data + [Preserve] public int? expirationDate { get; set; } - + [Ignore] public DateTime expirationDateFormatted { get; set; } } + [Preserve] public class offlineMaps { - [PrimaryKey, AutoIncrement] + [PrimaryKey, AutoIncrement, Preserve] public int id { get; set; } + [Preserve] public string name { get; set; } } + [Preserve] public class tile2offline { + [Preserve] public int tileId { get; set; } + [Preserve] public int mapId { get; set; } } } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs index 88faecd83..9cff1d5cc 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/SQLiteCache/Tilesets.cs @@ -1,20 +1,26 @@ using Mapbox.BaseModule.Data.Platform.SQLite; +using UnityEngine.Scripting; namespace Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache { /// - /// Don't change the class name: sqlite-net uses it for table creation + /// Don't change the class name: sqlite-net uses it for table creation. + /// [Preserve] forces IL2CPP managed-code-stripping to keep the property + /// accessors that sqlite-net invokes via reflection — without it, set_id is + /// stripped on Android Medium/High stripping and every row maps to id=0. /// + [Preserve] public class tilesets { //hrmpf: multiple PKs not supported by sqlite.net //https://github.com/praeclarum/sqlite-net/issues/282 //TODO: do it via plain SQL - [PrimaryKey, AutoIncrement] + [PrimaryKey, AutoIncrement, Preserve] public int id { get; set; } + [Preserve] public string name { get; set; } } } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs index fc0886034..877c1acc4 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Cache/TypeMemoryCache.cs @@ -87,12 +87,14 @@ public void Remove(CanonicalTileId tileId) { if (_active.TryGetValue(tileId, out var data)) { + data.Dispose(); _active.Remove(tileId); return; } if (_inactiveMap.TryGetValue(tileId, out var tuple)) { + tuple.Value.value.Dispose(); _inactiveMap.Remove(tileId); _inactiveList.Remove(tuple); return; @@ -100,6 +102,7 @@ public void Remove(CanonicalTileId tileId) if (_fallbackDatas.TryGetValue(tileId, out var fallback)) { + fallback.Dispose(); _fallbackDatas.Remove(tileId); } } @@ -159,6 +162,16 @@ public void MarkFallback(CanonicalTileId dataTileId) _fallbackDatas.Add(dataTileId, tuple.Value.value); } } + + public void ClearInactive() + { + foreach (var tileData in _inactiveList) + { + tileData.value.Dispose(); + } + _inactiveMap.Clear(); + _inactiveList.Clear(); + } public IEnumerable GetAllDatas() { @@ -200,6 +213,9 @@ public void OnDestroy() public int ActiveCount => _active.Count; public int InactiveCount => _inactiveMap.Count; + + public Dictionary GetActiveData => _active; + public Dictionary GetFallbackData => _fallbackDatas; } public interface ITypeCache diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs b/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs index fe2c6e299..075c801fe 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/Response.cs @@ -28,7 +28,7 @@ public class Response { - private Response() { } + public Response() { } public IAsyncRequest Request { get; private set; } diff --git a/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs b/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs index e83bb3fbc..17fdc36c9 100644 --- a/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs +++ b/Runtime/Mapbox/BaseModule/Data/Platform/TileJSON/TileJSONReponse.cs @@ -2,6 +2,7 @@ using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using Newtonsoft.Json; +using UnityEngine.Scripting; namespace Mapbox.BaseModule.Data.Platform.TileJSON { @@ -137,6 +138,7 @@ public long? Modified [JsonProperty("webpage")] public string WebPage { get; set; } - + [Preserve] + public TileJSONResponse() { } } } diff --git a/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs b/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs index 16175d18f..59b22ad0d 100644 --- a/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs +++ b/Runtime/Mapbox/BaseModule/Data/Tasks/MeshGenTaskWrapper.cs @@ -4,11 +4,11 @@ namespace Mapbox.BaseModule.Data.Tasks { - public class MeshGenTaskWrapper : TaskWrapper + public class MeshGenTaskWrapper : TaskWrapper where T : MeshGenTaskWrapperResult, new() { - public MeshGenTaskWrapperResult DataResult; - public Func DataAction; - public Action DataCompleted; + public T DataResult; + public Func DataAction; + public Action DataCompleted; public override void Action() { @@ -20,18 +20,19 @@ public override void Completed(Task task) if (task != null && !task.IsFaulted) { DataCompleted(Task.CompletedTask, DataResult); + IsCompleted = true; return; } - + if (task == null) { - DataResult ??= new MeshGenTaskWrapperResult(); + DataResult ??= new T(); DataResult.ResultType = TaskResultType.Cancelled; DataCompleted(null, DataResult); } else { - DataResult ??= new MeshGenTaskWrapperResult(); + DataResult ??= new T(); DataResult.ResultType = TaskResultType.MeshGenerationFailure; DataCompleted(task, DataResult); } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs index ff1528ee6..2ec17b10b 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/ElevatedTerrainStrategy.cs @@ -1,9 +1,17 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using Mapbox.ImageModule.Terrain.Settings; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; +using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.ImageModule.Terrain.TerrainStrategies { @@ -29,16 +37,63 @@ public class ElevatedTerrainStrategy : TerrainStrategy, IElevationBasedTerrainSt private List _newVertexList; private List _newNormalList; private List _newUvList; - //private List _newTriangleList; - private Vector3 _newDir; - private int _vertA, _vertB, _vertC; - private int _counter; private bool _useTileSkirts = false; private float _skirtSize = 1; private int _sideVertexCount; private int _requiredVertexCount; + + private TerrainColliderOptions _colliderOptions; + + // Collider geometry is written by a Burst job into persistent NativeArrays so the + // data flows zero-copy into Mesh.SetVertices / SetIndices. Triangles are generated + // once per sampleCount change (they don't depend on elevation), so only the vertex + // buffer is written per tile build. The elevation mirror is a NativeArray copy of + // the current data tile's managed ElevationValues — populated lazily when the + // source reference changes, reused across the 16 render tiles that share one data + // tile. + private NativeArray _colliderVerticesNative; + private NativeArray _colliderTrianglesNative; + private int _colliderNativeSampleCount = -1; + private NativeArray _elevationNative; + // Keyed by the TerrainData reference rather than the float[] reference: ElevationArrayPool + // recycles float[] buffers, so the same array reference can hold different data across + // dispose+re-rent cycles. TerrainData instances aren't recycled the same way, so reference + // equality on the data wrapper is a safe cache key. + private TerrainData _elevationNativeMirror; + + // Shared flat render mesh used by every tile in shader mode. All render tiles have + // byte-identical CPU vertex data in that mode (the per-tile _HeightTexture_ST + // sub-region determines the actual surface); pointing every MeshFilter at this + // single Mesh avoids per-tile Mesh allocation and upload. + // Exposed for UnityMapTile.OnDestroy so its leak cleanup can identify which meshes + // it must skip (this one is owned by the strategy) versus destroy (per-tile collider + // or CPU-elevated render mesh). + public const string SharedFlatMeshName = "TerrainSharedFlat"; + public const string TerrainColliderMeshName = "TerrainCollider"; + public const string TerrainCpuMeshName = "TerrainCpuMesh"; + private Mesh _sharedFlatMesh; + + // Tracks the (TerrainData, CanonicalTileId) last used to build each MeshCollider's + // sharedMesh. Temp-tile → final-tile transitions can trigger RegisterTile with the + // same data; skipping the rebuild avoids a redundant PhysX re-cook. + private readonly Dictionary + _lastColliderBuild = new Dictionary(); + + // Sweep dead-collider entries from _lastColliderBuild once it exceeds this count. + // Bounded by tile-pool peak in normal use; the sweep catches the case where + // GameObjects actually destroy (scene teardown, runtime tile-pool resize). + private const int LastColliderBuildSweepThreshold = 256; + private readonly List _deadColliderKeysScratch = new List(); + + // Tracks deferred-rebuild closures so OnDestroy can unsubscribe them. Without this + // list, if a TerrainData is disposed before ElevationValuesUpdated fires, the + // closure stays attached to the event and roots both tile + data via capture. + // Keyed by (data, tile) so RegisterCollider's temp→final re-invocation can + // de-dup against an already-pending entry. + private readonly List<(TerrainData data, UnityMapTile tile, Action rebuild)> _pendingRebuilds = + new List<(TerrainData, UnityMapTile, Action)>(); public override int RequiredVertexCount { @@ -56,17 +111,72 @@ public override void Initialize(ElevationLayerProperties elOptions) } _useTileSkirts = elOptions.sideWallOptions.isActive; - _sideVertexCount = _useTileSkirts - ? _elevationOptions.modificationOptions.sampleCount + 3 + _sideVertexCount = _useTileSkirts + ? _elevationOptions.modificationOptions.sampleCount + 3 : _elevationOptions.modificationOptions.sampleCount + 1; _skirtSize = elOptions.sideWallOptions.wallHeight; - + _colliderOptions = elOptions.colliderOptions; + _newVertexList = new List(_requiredVertexCount); _newNormalList = new List(_requiredVertexCount); _newUvList = new List(_requiredVertexCount); - + _baseMesh = CreateBaseMesh(_elevationOptions.TileMeshSize, _sideVertexCount); _requiredVertexCount = _baseMesh.Vertices.Length; + + // Free the previous shared mesh on re-init — otherwise a second Initialize + // orphans it. OnDestroy alone isn't enough since re-init can happen without a + // preceding destroy (settings changes, editor reloads). + if (_sharedFlatMesh != null) + { + UnityEngine.Object.Destroy(_sharedFlatMesh); + _sharedFlatMesh = null; + } + + // Build the shader-mode shared render mesh from the base data once. Every + // shader-mode tile's MeshFilter will point at this single instance. + _sharedFlatMesh = new Mesh { name = SharedFlatMeshName }; + _sharedFlatMesh.subMeshCount = 2; + _sharedFlatMesh.vertices = _baseMesh.Vertices; + _sharedFlatMesh.normals = _baseMesh.Normals; + for (var i = 0; i < _baseMesh.Triangles.Count; i++) + { + _sharedFlatMesh.SetTriangles(_baseMesh.Triangles[i], i); + } + _sharedFlatMesh.uv = _baseMesh.Uvs; + _sharedFlatMesh.UploadMeshData(markNoLongerReadable: false); + } + + /// + /// Cascaded from TerrainLayerModule.OnDestroy. Releases the shared render + /// mesh and the persistent NativeArrays that back Burst collider builds; per-tile + /// collider meshes are released from UnityMapTile.OnDestroy. + /// + public override void OnDestroy() + { + // Detach any pending-rebuild subscriptions before disposing — closures + // would otherwise stay attached to TerrainData.ElevationValuesUpdated and + // root captured (tile, data) indefinitely if the event never fires. + for (int i = 0; i < _pendingRebuilds.Count; i++) + { + var entry = _pendingRebuilds[i]; + if (entry.data != null && entry.rebuild != null) + { + entry.data.ElevationValuesUpdated -= entry.rebuild; + } + } + _pendingRebuilds.Clear(); + + if (_sharedFlatMesh != null) + { + UnityEngine.Object.Destroy(_sharedFlatMesh); + _sharedFlatMesh = null; + } + if (_colliderVerticesNative.IsCreated) _colliderVerticesNative.Dispose(); + if (_colliderTrianglesNative.IsCreated) _colliderTrianglesNative.Dispose(); + if (_elevationNative.IsCreated) _elevationNative.Dispose(); + _lastColliderBuild.Clear(); + _elevationNativeMirror = null; } public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) @@ -76,10 +186,46 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) tile.gameObject.layer = _elevationOptions.unityLayerOptions.layerId; } - if (tile.MeshVertexCount != RequiredVertexCount) + if (!createElevatedMesh) + { + // Shader mode: point the tile at the shared flat mesh. Byte-identical + // vertex data across tiles means a single Mesh covers the whole pool; the + // shader picks the right sub-region via per-tile _HeightTexture_ST. + if (tile.MeshFilter.sharedMesh != _sharedFlatMesh) + { + var previous = tile.MeshFilter.sharedMesh; + tile.MeshFilter.sharedMesh = _sharedFlatMesh; + // The mesh UnityMapTile.Awake allocated is now orphaned; destroy it + // unless something else (not us) already put the shared mesh here. + if (previous != null && previous != _sharedFlatMesh && previous.name != SharedFlatMeshName) + { + UnityEngine.Object.Destroy(previous); + } + } + tile.MeshVertexCount = _sharedFlatMesh.vertexCount; + } + else if (tile.MeshVertexCount != RequiredVertexCount) { - Mesh sharedMesh; - (sharedMesh = tile.MeshFilter.sharedMesh).Clear(); + // CPU mode: each tile needs its own unique vertex buffer since the Y + // displacement is per-tile. Reset the tile's Awake-allocated mesh from the + // base template — but never Clear() _sharedFlatMesh, which would wipe the + // mesh used by every shader-mode tile. Allocate a fresh per-tile mesh if + // this tile's MeshFilter is still pointing at the shared instance (happens + // when a runtime config change flips the tile shader→CPU and the new + // RequiredVertexCount differs from _sharedFlatMesh.vertexCount). + var sharedMesh = tile.MeshFilter.sharedMesh; + if (sharedMesh == null || sharedMesh == _sharedFlatMesh) + { + sharedMesh = new Mesh { name = TerrainCpuMeshName }; + tile.MeshFilter.sharedMesh = sharedMesh; + } + else + { + // Re-tag any pre-existing per-tile mesh (e.g. the Awake mesh) so + // UnityMapTile.OnDestroy correctly destroys it on tile teardown. + sharedMesh.name = TerrainCpuMeshName; + } + sharedMesh.Clear(); var newMesh = _baseMesh; sharedMesh.subMeshCount = 2; sharedMesh.vertices = newMesh.Vertices; @@ -93,18 +239,505 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) tile.MeshVertexCount = newMesh.Vertices.Length; tile.ElevationUpdatedCallback(); } - + if (createElevatedMesh) { + // Same-vertex-count shader→CPU transition: the else-if branch above didn't + // fire, but MeshFilter.sharedMesh might still be _sharedFlatMesh. Allocate + // a per-tile owned mesh in that case so CreateElevatedMesh's writes don't + // hit the shared one. + if (tile.MeshFilter.sharedMesh == null || tile.MeshFilter.sharedMesh == _sharedFlatMesh) + { + var perTile = new Mesh { name = TerrainCpuMeshName }; + perTile.subMeshCount = 2; + perTile.vertices = _baseMesh.Vertices; + perTile.normals = _baseMesh.Normals; + for (var i = 0; i < _baseMesh.Triangles.Count; i++) + { + perTile.SetTriangles(_baseMesh.Triangles[i], i); + } + perTile.uv = _baseMesh.Uvs; + tile.MeshFilter.sharedMesh = perTile; + } CreateElevatedMesh(tile); } + + if (_colliderOptions != null && _colliderOptions.addCollider) + { + RegisterCollider(tile); + } + } + + /// + /// Ensures has a backed by a + /// CPU-elevated mesh. Builds immediately when elevation data is already decoded, + /// otherwise defers until fires + /// (async GPU-readback path). Short-circuits when the tile's existing collider was + /// already built from the same data + tileId (temp→final transitions trigger + /// RegisterTile multiple times without the underlying data changing). + /// + private void RegisterCollider(UnityMapTile tile) + { + var data = tile.TerrainContainer != null ? tile.TerrainContainer.TerrainData : null; + if (data == null) + { + return; + } + + var existing = FindExistingCollider(tile); + if (existing != null && + _lastColliderBuild.TryGetValue(existing, out var last) && + ReferenceEquals(last.data, data) && + last.tileId.Equals(tile.CanonicalTileId)) + { + return; + } + + if (data.IsElevationDataReady) + { + BuildAndAssignCollider(tile); + } + else + { + // De-dup: temp→final transitions re-call RegisterTile with the same + // (tile, data). Without this check we'd add a second closure + a second + // async bake, doubling work for every transition. + for (int i = 0; i < _pendingRebuilds.Count; i++) + { + if (ReferenceEquals(_pendingRebuilds[i].data, data) && + ReferenceEquals(_pendingRebuilds[i].tile, tile)) + { + return; + } + } + + // Defer until values arrive. The callback self-unsubscribes on fire and + // no-ops if the tile has since been recycled onto different data, so a late + // async readback does not stomp a freshly-reassigned tile. We also track + // the subscription in _pendingRebuilds so OnDestroy can detach pending + // closures whose data is disposed before the event fires, AND attach a + // dispose-callback so eviction self-detaches (TerrainData.Dispose doesn't + // raise ElevationValuesUpdated, so without this hook the closure would + // stay attached until the next visualizer teardown — pendings grow + // O(failed-decodes) under bursty zoom). + Action rebuild = null; + Action onDispose = null; + (TerrainData data, UnityMapTile tile, Action rebuild) entry = (data, tile, null); + rebuild = () => + { + data.ElevationValuesUpdated -= rebuild; + data.RemoveDisposeCallback(onDispose); + _pendingRebuilds.Remove(entry); + if (tile == null || tile.TerrainContainer == null || tile.TerrainContainer.TerrainData != data) + { + return; + } + BuildAndAssignCollider(tile); + }; + onDispose = () => + { + data.ElevationValuesUpdated -= rebuild; + data.RemoveDisposeCallback(onDispose); + _pendingRebuilds.Remove(entry); + }; + entry.rebuild = rebuild; + _pendingRebuilds.Add(entry); + data.ElevationValuesUpdated += rebuild; + data.AddDisposeCallback(onDispose); + } + } + + // Our generated grid has no duplicate verts, no degenerate triangles, and doesn't + // need welding, so we tell PhysX to skip those cook stages. Keeps + // CookForFasterSimulation on for faster runtime queries against the static terrain. + private const MeshColliderCookingOptions TerrainColliderCookingOptions = + MeshColliderCookingOptions.CookForFasterSimulation | + MeshColliderCookingOptions.UseFastMidphase; + + // Name of the dedicated child GameObject that holds the MeshCollider when + // useDedicatedColliderLayer is enabled. Keyed by name so we can locate + reuse it + // across pool cycles without maintaining a per-tile dictionary. Distinct from + // TerrainColliderMeshName so the GO name and the Mesh name don't collide + // (they were both "TerrainCollider" — same string used for two different purposes). + private const string ColliderChildName = "TerrainColliderChild"; + + // Removes _lastColliderBuild entries whose MeshCollider key has been destroyed + // (GameObject teardown, scene unload). Unity's overloaded == returns true for + // destroyed-vs-null, but Dictionary hashes by C# reference equality and + // would otherwise keep dead proxies indefinitely. + private void SweepDeadColliderEntries() + { + _deadColliderKeysScratch.Clear(); + foreach (var kv in _lastColliderBuild) + { + if (kv.Key == null) + { + _deadColliderKeysScratch.Add(kv.Key); + } + } + for (int i = 0; i < _deadColliderKeysScratch.Count; i++) + { + _lastColliderBuild.Remove(_deadColliderKeysScratch[i]); + } + _deadColliderKeysScratch.Clear(); + } + + /// + /// Looks up the existing for this tile without creating + /// one, mirroring the location rule in . Used by + /// the rebuild-short-circuit check. + /// + private MeshCollider FindExistingCollider(UnityMapTile tile) + { + if (_colliderOptions.useDedicatedColliderLayer) + { + var childTransform = tile.transform.Find(ColliderChildName); + return childTransform != null ? childTransform.GetComponent() : null; + } + return tile.GetComponent(); + } + + /// + /// Returns the the collider mesh should be assigned to. + /// When is enabled + /// the collider lives on a child GameObject so it can sit on its own Unity Layer + /// independently of the tile's render layer; otherwise it's attached to the tile + /// itself. Applies our tuned on first + /// creation. + /// + private MeshCollider GetOrCreateCollider(UnityMapTile tile) + { + MeshCollider collider; + if (_colliderOptions.useDedicatedColliderLayer) + { + var childTransform = tile.transform.Find(ColliderChildName); + GameObject childGo; + if (childTransform == null) + { + childGo = new GameObject(ColliderChildName); + childGo.transform.SetParent(tile.transform, worldPositionStays: false); + } + else + { + childGo = childTransform.gameObject; + } + childGo.layer = _colliderOptions.colliderLayerId; + + collider = childGo.GetComponent(); + if (collider == null) + { + collider = childGo.AddComponent(); + } + } + else + { + collider = tile.GetComponent(); + if (collider == null) + { + collider = tile.gameObject.AddComponent(); + } + } + // Reassign every call: if the collider's cookingOptions diverges from what + // BakeColliderJob uses, PhysX discards the async-cooked cache and re-cooks + // synchronously on sharedMesh assignment — silently defeating the async path. + collider.cookingOptions = TerrainColliderCookingOptions; + return collider; + } + + /// + /// Builds a dedicated CPU-elevated collider mesh for and + /// assigns it to the tile's . Grid resolution mirrors the + /// render mesh so physics stays visually aligned with the terrain surface. + /// + private void BuildAndAssignCollider(UnityMapTile tile) + { + var meshCollider = GetOrCreateCollider(tile); + + // Unity auto-populates sharedMesh on a newly added MeshCollider from the + // GameObject's MeshFilter.sharedMesh. We must NOT write into that mesh — it is + // the render mesh. Detect and allocate a dedicated collider Mesh instead. + var mesh = meshCollider.sharedMesh; + if (mesh == null || mesh == tile.MeshFilter.sharedMesh) + { + mesh = new Mesh { name = TerrainColliderMeshName }; + mesh.MarkDynamic(); + } + + // Grid resolution matches the render mesh's INTERIOR vertex grid so the + // collision surface aligns with the visible terrain over the tile area. In + // skirt mode the render mesh adds 2 extra verts per axis (xrat = -0.01, + // 1.01) just outside the tile boundary; the collider intentionally omits + // these — there's no real geometry at the skirt and physics queries are not + // expected outside the tile. If the user picks a coarser SimplificationFactor, + // the collider follows. + var sampleCount = _elevationOptions.modificationOptions.sampleCount; + var side = sampleCount + 1; + var size = _elevationOptions.TileMeshSize; + var scale = tile.TileScale; + + // Elevation mirror is populated once per data tile and shared across the 16 + // render tiles that sample the same data. Triangles are regenerated only when + // sampleCount changes — see EnsureColliderBuffers. + var container = tile.TerrainContainer; + var terrainData = container.TerrainData; + var elevationValues = terrainData.ElevationValues; + var dataWidth = (int)Mathf.Sqrt(elevationValues.Length); + var scaleOffset = container.TerrainTextureScaleOffset; + + EnsureColliderBuffers(sampleCount); + EnsureElevationNative(terrainData); + + new BuildColliderVerticesJob + { + ElevationValues = _elevationNative, + DataWidth = dataWidth, + SampleCount = sampleCount, + Side = side, + Size = size, + Scale = scale, + SectionWidth = dataWidth * scaleOffset.x - 1f, + PaddingX = dataWidth * scaleOffset.z, + PaddingY = dataWidth * scaleOffset.w, + Vertices = _colliderVerticesNative + }.Run(); + + mesh.Clear(); + mesh.SetVertices(_colliderVerticesNative); + // SetIndices is the canonical NativeArray-taking Mesh index API; the + // NativeArray overload of SetTriangles isn't present on every Unity 2022.3 + // point release. MeshTopology.Triangles produces equivalent triangle-list + // geometry. + mesh.SetIndices(_colliderTrianglesNative, MeshTopology.Triangles, 0, calculateBounds: false); + mesh.RecalculateBounds(); + + // Record what we just built so RegisterCollider can short-circuit on + // redundant subsequent RegisterTile calls (temp→final transitions). + // Sweep dead Unity Object keys opportunistically when the dict grows past + // the soft cap so destroyed-collider proxies don't linger across long + // sessions (Unity's overloaded == doesn't reach Dictionary's reference + // equality, so dead keys would otherwise stay queryable). + if (_lastColliderBuild.Count > LastColliderBuildSweepThreshold) + { + SweepDeadColliderEntries(); + } + _lastColliderBuild[meshCollider] = (tile.TerrainContainer.TerrainData, tile.CanonicalTileId); + + if (_colliderOptions.asyncBakeCollider) + { + // Move the PhysX cook to a worker thread. The assignment (and thus any + // implicit recook) happens one frame later once BakeMesh has populated the + // native cooked-data cache for this mesh id. PhysX reuses that cache when + // sharedMesh is assigned, so the main-thread step is near-free. + var handle = new BakeColliderJob + { + MeshId = mesh.GetInstanceID(), + CookingOptions = TerrainColliderCookingOptions + }.Schedule(); + Runnable.Instance.StartCoroutine(CompleteBakeAndAssign(meshCollider, mesh, handle)); + } + else + { + // Null-then-reassign forces PhysX to re-cook collision data; a same-reference + // reassignment is a no-op. + meshCollider.sharedMesh = null; + meshCollider.sharedMesh = mesh; + } + } + + /// + /// (Re)allocates the persistent collider NativeArrays when the strategy's + /// sampleCount changes, and generates the triangle index list once — triangles are + /// a pure function of and never need rebuilding per + /// tile. + /// + private void EnsureColliderBuffers(int sampleCount) + { + if (_colliderNativeSampleCount == sampleCount && _colliderVerticesNative.IsCreated && _colliderTrianglesNative.IsCreated) + { + return; + } + if (_colliderVerticesNative.IsCreated) _colliderVerticesNative.Dispose(); + if (_colliderTrianglesNative.IsCreated) _colliderTrianglesNative.Dispose(); + + var side = sampleCount + 1; + _colliderVerticesNative = new NativeArray(side * side, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + _colliderTrianglesNative = new NativeArray(sampleCount * sampleCount * 6, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + + int ti = 0; + for (int y = 0; y < sampleCount; y++) + { + for (int x = 0; x < sampleCount; x++) + { + int vertA = y * side + x; + int vertB = vertA + side + 1; + int vertC = vertA + side; + _colliderTrianglesNative[ti++] = vertA; + _colliderTrianglesNative[ti++] = vertC; + _colliderTrianglesNative[ti++] = vertB; + + vertA = y * side + x; + vertB = vertA + 1; + vertC = vertA + side + 1; + _colliderTrianglesNative[ti++] = vertA; + _colliderTrianglesNative[ti++] = vertC; + _colliderTrianglesNative[ti++] = vertB; + } + } + _colliderNativeSampleCount = sampleCount; + } + + /// + /// Mirrors the current data tile's managed ElevationValues into the + /// strategy's persistent . Reuses the existing + /// NativeArray when the source reference is unchanged, + /// so the 16 render tiles that share a data tile pay the ~256 KB copy only once. + /// + private void EnsureElevationNative(TerrainData terrainData) + { + var source = terrainData.ElevationValues; + if (ReferenceEquals(terrainData, _elevationNativeMirror) && _elevationNative.IsCreated && _elevationNative.Length == source.Length) + { + return; + } + if (_elevationNative.IsCreated && _elevationNative.Length != source.Length) + { + _elevationNative.Dispose(); + } + if (!_elevationNative.IsCreated) + { + _elevationNative = new NativeArray(source.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + } + _elevationNative.CopyFrom(source); + _elevationNativeMirror = terrainData; + } + + /// + /// Burst-compiled vertex fill for the collider mesh. Bilinearly samples the + /// mirrored elevation NativeArray and writes world-local vertex positions. Grid + /// topology (triangles) is produced once in , + /// not in this job. + /// + [BurstCompile] + private struct BuildColliderVerticesJob : IJob + { + [ReadOnly] public NativeArray ElevationValues; + public int DataWidth; + public int SampleCount; + public int Side; + public float Size; + public float Scale; + public float SectionWidth; + public float PaddingX; + public float PaddingY; + public NativeArray Vertices; + + public void Execute() + { + var invSampleCount = 1f / SampleCount; + var maxIndex = DataWidth - 1; + + for (int y = 0; y < Side; y++) + { + var yrat = y * invSampleCount; + var sampleYf = PaddingY + yrat * SectionWidth; + if (sampleYf < 0f) sampleYf = 0f; else if (sampleYf > maxIndex) sampleYf = maxIndex; + var y0 = (int)sampleYf; + var y1 = y0 + 1; if (y1 > maxIndex) y1 = maxIndex; + var fy = sampleYf - y0; + var row0 = y0 * DataWidth; + var row1 = y1 * DataWidth; + var yy = (1f - yrat) * Size; + + for (int x = 0; x < Side; x++) + { + var xrat = x * invSampleCount; + var sampleXf = PaddingX + xrat * SectionWidth; + if (sampleXf < 0f) sampleXf = 0f; else if (sampleXf > maxIndex) sampleXf = maxIndex; + var x0 = (int)sampleXf; + var x1 = x0 + 1; if (x1 > maxIndex) x1 = maxIndex; + var fx = sampleXf - x0; + + var h00 = ElevationValues[row0 + x0]; + var h10 = ElevationValues[row0 + x1]; + var h01 = ElevationValues[row1 + x0]; + var h11 = ElevationValues[row1 + x1]; + var h0 = h00 + (h10 - h00) * fx; + var h1 = h01 + (h11 - h01) * fx; + var sample = h0 + (h1 - h0) * fy; + + Vertices[y * Side + x] = new Vector3(xrat * Size, sample * Scale, -yy); + } + } + } + } + + /// + /// IJob wrapper around Physics.BakeMesh so the PhysX cook can run on a + /// worker thread. The cooking options must match what the MeshCollider is + /// configured with, otherwise PhysX discards the cached data and re-cooks on + /// assignment. + /// Intentionally NOT [BurstCompile]: the entire body is a single P/Invoke into + /// PhysX, so Burst-compiling adds AOT cost with no measurable benefit. + /// + private struct BakeColliderJob : IJob + { + public int MeshId; + public MeshColliderCookingOptions CookingOptions; + + public void Execute() + { + Physics.BakeMesh(MeshId, false, CookingOptions); + } + } + + /// + /// Waits for an async to complete, then assigns the + /// pre-cooked mesh to its . Handles: (a) the tile or + /// collider got destroyed during the bake — we free the orphaned mesh so it doesn't + /// leak; (b) a previous TerrainCollider mesh was attached to this collider — + /// we destroy it once we've swapped in the new one, since each async build + /// allocates a fresh Mesh. + /// + private static IEnumerator CompleteBakeAndAssign(MeshCollider meshCollider, Mesh mesh, JobHandle handle) + { + while (!handle.IsCompleted) + { + yield return null; + } + handle.Complete(); + + // Either side can be destroyed while we waited: UnityMapTile.OnDestroy can take + // the meshCollider with the GameObject, and a parallel bake completion that + // landed first can have Destroy()'d our mesh as its "previous" (line below). + if (meshCollider == null || mesh == null) + { + if (mesh != null) + { + UnityEngine.Object.Destroy(mesh); + } + yield break; + } + + var previous = meshCollider.sharedMesh; + meshCollider.sharedMesh = null; + meshCollider.sharedMesh = mesh; + if (previous != null && previous != mesh && previous.name == TerrainColliderMeshName) + { + UnityEngine.Object.Destroy(previous); + } } private void CreateElevatedMesh(UnityMapTile tile) { - var mesh = tile.MeshFilter.mesh; + // Use sharedMesh (not .mesh) — the .mesh getter implicitly clones the assigned + // shared mesh on first access and orphans the original. We pre-ensured a + // uniquely-owned per-tile mesh in RegisterTile above. + var mesh = tile.MeshFilter.sharedMesh; var vertices = mesh.vertices; - var sampleCount = (int)Mathf.Sqrt(mesh.vertexCount); + // Per-axis vertex count is cached from Initialize. Was previously derived via + // Mathf.Sqrt(mesh.vertexCount), which is exact for our square grid but wasteful + // and gave the local an off-axis name (it's _sideVertexCount, not sampleCount). + var sampleCount = _sideVertexCount; for (int i = 0; i < vertices.Length; i++) { var x = i % sampleCount; @@ -125,7 +758,13 @@ private void CreateElevatedMesh(UnityMapTile tile) vertices[i].Set(vertices[i].x, elevation, vertices[i].z); } - mesh.vertices = vertices; + mesh.SetVertices(vertices); + // Actual displaced bounds are tighter than the min/max-padded fallback bounds + // set by UnityMapTile.ElevationUpdatedCallback. Mesh normals/tangents are not + // recalculated: the terrain shader derives the surface normal from the height + // texture directly in the fragment stage, so the mesh's vertex normals are + // never read in either mode. + mesh.RecalculateBounds(); } #region mesh gen @@ -200,6 +839,37 @@ private MeshDataArray CreateBaseMeshWithoutSkirts(float size, int sampleCount) return mesh; } + /// + /// Fraction of tile size that the outer skirt ring extends past the tile boundary. + /// Kept constant so coarse meshes (e.g. sampleCount=2 with + /// SimplificationFactor=64) don't balloon the skirt half-way into neighboring + /// tiles. 1% is narrow enough to be invisible at typical zoom levels and wide enough + /// to hide seam artifacts from float-precision mismatches. + /// + private const float SkirtOuterOffsetFraction = 0.01f; + + /// + /// Maps a skirt-loop index (-1 .. sideVertexCount-2) to the [0,1] UV range + /// used for interior vertices, with the outer ring pinned to a fixed offset outside + /// the tile rather than one grid step. Without this, a 3x3 grid puts the skirt half + /// a tile outside the edge and overlaps neighboring tiles. + /// + /// Loop index. -1 is the left/top outer skirt row; sideVertexCount-2 is the right/bottom outer skirt row. + /// Number of grid segments along one tile side (sampleCount). + /// Total verts per tile axis including skirts. + private static float RatioForSkirtIndex(int index, int interiorSteps, int sideVertexCount) + { + if (index == -1) + { + return -SkirtOuterOffsetFraction; + } + if (index == sideVertexCount - 2) + { + return 1f + SkirtOuterOffsetFraction; + } + return (float)index / interiorSteps; + } + private MeshDataArray CreateBaseMeshSkirts(float size, int sideVertexCount) { //TODO use arrays instead of lists @@ -207,16 +877,17 @@ private MeshDataArray CreateBaseMeshSkirts(float size, int sideVertexCount) _newNormalList.Clear(); _newUvList.Clear(); var _newTriangleList = new List(); + var interiorSteps = sideVertexCount - 3; //012 //345 //678 for (int y = -1; y < sideVertexCount - 1; y++) { - var yrat = (float)y / (sideVertexCount - 3); // 1 for buffer pixel, 2 for skirts + var yrat = RatioForSkirtIndex(y, interiorSteps, sideVertexCount); for (int x = -1; x < sideVertexCount - 1; x++) { - var xrat = (float)x / (sideVertexCount - 3); + var xrat = RatioForSkirtIndex(x, interiorSteps, sideVertexCount); var xx = Mathf.LerpUnclamped(0, size, xrat); //lerp x/y swapped here because of the texture space conversion (y to -y) @@ -281,9 +952,8 @@ private MeshDataArray CreateBaseMeshSkirts(float size, int sideVertexCount) mesh.Vertices = _newVertexList.ToArray(); mesh.Normals = _newNormalList.ToArray(); mesh.Uvs = _newUvList.ToArray(); - topQuadTris.AddRange(_newTriangleList.ToArray()); + topQuadTris.AddRange(_newTriangleList); mesh.Triangles.Add(topQuadTris.ToArray()); - //mesh.Triangles.Add(_newTriangleList.ToArray()); return mesh; } #endregion diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs index 8d94586a0..fbe2223f5 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationLayerProperties.cs @@ -1,14 +1,30 @@ -using System; +using System; using Mapbox.BaseModule.Utilities; +using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Bundle of sub-option groups consumed by implementations + /// to build per-tile terrain geometry. Passed through + /// TerrainStrategy.Initialize. + /// [Serializable] - public class ElevationLayerProperties + public class ElevationLayerProperties { + [Tooltip("Controls the terrain render mesh grid density via a simplification factor. Expand to pick a quality preset.")] public ElevationModificationOptions modificationOptions = new ElevationModificationOptions(); + + [Tooltip("Optional Unity layer assignment for every terrain tile GameObject (useful for culling, raycast masks, lighting).")] public UnityLayerOptions unityLayerOptions = new UnityLayerOptions(); + + [Tooltip("Optional downward side-wall skirts around each tile that hide horizontal gaps between neighboring tiles at different elevations.")] public TerrainSideWallOptions sideWallOptions = new TerrainSideWallOptions(); + + [Tooltip("Optional MeshCollider generation for terrain tiles so physics can interact with the map surface.")] + public TerrainColliderOptions colliderOptions = new TerrainColliderOptions(); + + [Tooltip("World-unit size of a single terrain tile quad along X and Z. Default 1 matches the map's internal tile scale; change only if you know why.")] public float TileMeshSize = 1; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs index 8343f9966..fa32fc0af 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/ElevationModificationOptions.cs @@ -1,14 +1,24 @@ -using System; +using System; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Grid density for the terrain render mesh. The value chosen here propagates to the + /// collider and CPU elevation sampling logic, so changing it affects both visuals and + /// physics resolution. + /// [Serializable] public class ElevationModificationOptions { - [Tooltip("Results in 128/n x 128/n grid")] - [Range(1,128)] + [Tooltip("Divisor applied to 128 to determine terrain render-mesh grid density. Pick from the dropdown: 1 = finest (129x129 verts, heaviest), 128 = coarsest (2x2 verts, fastest). Very coarse values show visible seams; the inspector warns you. Default: 1.")] + [SimplificationFactor(1, 128)] public int SimplificationFactor = 1; + + /// + /// Convenience accessor: number of grid segments per tile side. Equals + /// 128 / SimplificationFactor. Vertex count per side is sampleCount + 1. + /// public int sampleCount => 128 / SimplificationFactor; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs new file mode 100644 index 000000000..ddf29aaf3 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace Mapbox.ImageModule.Terrain.Settings +{ + /// + /// Marks an field as a terrain grid-simplification factor. In the + /// Inspector this renders as a dropdown of presets (each halving the grid resolution) + /// plus a live help box that shows the resulting grid size and warns at coarse values. + /// Legacy non-preset values are snapped to the nearest preset on first inspector render. + /// The drawer lives in the editor assembly. + /// + public class SimplificationFactorAttribute : PropertyAttribute + { + /// Smallest allowed factor value (finest grid). + public readonly int Min; + + /// Largest allowed factor value (coarsest grid). + public readonly int Max; + + /// Number divided by the factor to produce the sample (segment) count. Usually 128. + public readonly int VertexBase; + + /// Smallest allowed factor value. + /// Largest allowed factor value. + /// Dividend for sample-count math. Usually 128. + public SimplificationFactorAttribute(int min, int max, int vertexBase = 128) + { + Min = min; + Max = max; + VertexBase = vertexBase; + } + } +} diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs.meta b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta similarity index 71% rename from Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs.meta rename to Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta index 78635c575..d5fbc2709 100644 --- a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs.meta +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/SimplificationFactorAttribute.cs.meta @@ -1,7 +1,5 @@ fileFormatVersion: 2 -guid: d3d557417079b1446999d2d86ff71dfb -timeCreated: 1524214127 -licenseType: Pro +guid: d12c7339a206a4d8facdc684bd11ff46 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs index e15590c51..0aa94fc3b 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainColliderOptions.cs @@ -1,14 +1,29 @@ -using System; +using System; +using Mapbox.BaseModule.Utilities.Attributes; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Inspector options that control whether each terrain tile gets a MeshCollider. The + /// collider mesh is generated lazily from the CPU elevation data once it arrives, with + /// a grid resolution that mirrors the terrain render mesh + /// (). + /// [Serializable] - public class TerrainColliderOptions + public class TerrainColliderOptions { - [Tooltip("Add Unity Physics collider to terrain tiles, used for detecting collisions etc.")] + [Tooltip("Attach a MeshCollider to each terrain tile so physics and raycasts can hit the map surface. Forces ExtractCpuElevationData on in the parent module. The collider grid resolution mirrors the render mesh's SimplificationFactor; coarser render = coarser collider. Default: off.")] public bool addCollider = false; - + [Tooltip("Bake MeshCollider cook data on a worker thread via Physics.BakeMesh, then assign sharedMesh one frame later. Removes main-thread cook stalls during bursty tile loads at the cost of a one-frame latency before the collider is usable. Recommended on when Simplification Factor is low (dense grids with expensive cooks). Default: off.")] + public bool asyncBakeCollider = false; + + [Tooltip("Put the MeshCollider on a dedicated child GameObject with its own Unity Layer, so physics raycast filtering can be independent of the tile's render layer. Adds one extra GameObject per tile. When off, the collider sits on the tile GameObject and shares its Unity Layer. Default: off.")] + public bool useDedicatedColliderLayer = false; + + [GameObjectLayer] + [Tooltip("Unity Layer the child collider GameObject is placed on when Use Dedicated Collider Layer is enabled. Typical pattern: assign a \"Terrain\" layer here and filter your gameplay raycasts to that layer.")] + public int colliderLayerId = 0; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs index 2fc6c0e9b..d3cec2930 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/TerrainSideWallOptions.cs @@ -1,14 +1,20 @@ -using System; +using System; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Optional downward skirts extended slightly past each terrain tile's edges to hide + /// horizontal gaps that appear between neighboring tiles when their elevations don't + /// line up exactly (float precision, texture filtering, different zoom parents). + /// [Serializable] public class TerrainSideWallOptions { - [Tooltip("Adds side walls to terrain meshes, reduces visual artifacts.")] + [Tooltip("Enable downward skirts along each tile's four edges. Hides visible seams between tiles at different heights. A small visual overhead (~1% tile-size outer offset, a handful of extra triangles per tile). Default: off.")] public bool isActive = false; - [Tooltip("Height of side walls.")] + + [Tooltip("How far down (in world units) the skirts hang below the terrain surface. Larger = taller walls that hide bigger seams but may peek out from under thin terrain features. Default 0.1. Typical range 0.05 - 1.")] public float wallHeight = .1f; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs index 36a741ceb..53b25ae5d 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/Settings/UnityLayerOptions.cs @@ -1,14 +1,21 @@ -using System; +using System; +using Mapbox.BaseModule.Utilities.Attributes; using UnityEngine; namespace Mapbox.ImageModule.Terrain.Settings { + /// + /// Unity Layer assignment for terrain tile GameObjects. Useful when you want to apply a + /// custom raycast mask, lighting layer, or camera culling layer to terrain. + /// [Serializable] public class UnityLayerOptions { - [Tooltip("Add terrain tiles to Unity Layer")] + [Tooltip("When enabled, every terrain tile GameObject is moved to the Unity Layer selected below. Use this to carve terrain out of a raycast mask or put it on a dedicated lighting/culling layer. Default: off.")] public bool addToLayer = false; - [Tooltip("Unity Layer id to which terrain tiles will get added.")] + + [GameObjectLayer] + [Tooltip("Unity Layer that terrain tiles are assigned to when Add To Layer is enabled. The dropdown shows layers defined in your Project's Tags & Layers settings.")] public int layerId = 0; } } diff --git a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs index 1165c1450..b03c89fe8 100644 --- a/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Data/TerrainStrategies/TerrainStrategy.cs @@ -22,5 +22,14 @@ public virtual void RegisterTile(UnityMapTile tile, bool createElevatedMesh) { } + + /// + /// Release any Unity objects (Meshes, textures) the strategy created. Called by + /// TerrainLayerModule.OnDestroy. + /// + public virtual void OnDestroy() + { + + } } } diff --git a/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs b/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs index 16b8a90e1..35366fc05 100644 --- a/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs +++ b/Runtime/Mapbox/BaseModule/Data/Tiles/CanonicalTileId.cs @@ -216,7 +216,7 @@ public static Vector4 CalculateScaleOffsetAtZoom(this CanonicalTileId current, i return new Vector4(scale, scale, offsetX, offsetY); } - public static Vector4 CalculateTopRightScaleOffsetAtZoom(this CanonicalTileId current, int zoomDiff) + public static Vector4 CalculateTopLeftScaleOffsetAtZoom(this CanonicalTileId current, int zoomDiff) { var tileZoom = current.Z; diff --git a/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs b/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs index 5103bbdd1..0e581d68e 100644 --- a/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs +++ b/Runtime/Mapbox/BaseModule/Data/Vector2d/RectD.cs @@ -2,18 +2,18 @@ { public struct RectD { - public Vector2d TopLeft { get; private set; } - public Vector2d BottomRight { get; private set; } + public Vector2d TopLeft; + public Vector2d BottomRight; //size is absolute width&height so Min+size != max - public Vector2d Size { get; private set; } - public Vector2d Center { get; private set; } + public Vector2d Size; + public Vector2d Center; public RectD(Vector2d topLeft, Vector2d size) { TopLeft = topLeft; - BottomRight = topLeft + size; + BottomRight = new Vector2d(topLeft.x + size.x, topLeft.y + size.y); //topLeft + size; Center = new Vector2d(TopLeft.x + size.x / 2, TopLeft.y + size.y / 2); - Size = new Vector2d(Mathd.Abs(size.x), Mathd.Abs(size.y)); + Size = size; //new Vector2d(Mathd.Abs(size.x), Mathd.Abs(size.y)); } public bool Contains(Vector2d point) diff --git a/Runtime/Mapbox/VectorModule/CustomWorldObject.meta b/Runtime/Mapbox/BaseModule/Editor.meta similarity index 77% rename from Runtime/Mapbox/VectorModule/CustomWorldObject.meta rename to Runtime/Mapbox/BaseModule/Editor.meta index ba76bdd5a..28695b49d 100644 --- a/Runtime/Mapbox/VectorModule/CustomWorldObject.meta +++ b/Runtime/Mapbox/BaseModule/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: cca07adf6752a48239648242c4cb168a +guid: 88c1b74736534426ea94437fd425da25 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs b/Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs similarity index 100% rename from Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs rename to Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs diff --git a/Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs.meta b/Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs.meta similarity index 100% rename from Runtime/Mapbox/Example/Editor/MapInformationDrawer.cs.meta rename to Runtime/Mapbox/BaseModule/Editor/MapInformationDrawer.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef new file mode 100644 index 000000000..d2c79de57 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxBaseModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta new file mode 100644 index 000000000..1626f91e1 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/MapboxBaseModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 567de054dbe5948c0bc25053f1aa6581 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs new file mode 100644 index 000000000..3f00a43e2 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs @@ -0,0 +1,33 @@ +using Mapbox.BaseModule.Unity.ModuleBehaviours; +using UnityEditor; + +namespace Mapbox.BaseModule.Editor +{ + [CustomEditor(typeof(RuntimeCacheManagerBehaviour))] + public class RuntimeCacheManagerBehaviourEditor : UnityEditor.Editor + { + SerializedProperty createSqliteCache; + SerializedProperty useCustomName; + SerializedProperty customName; + SerializedProperty createFileCache; + + void OnEnable() + { + createSqliteCache = serializedObject.FindProperty("CreateSqliteCache"); + useCustomName = serializedObject.FindProperty("UseCustomName"); + customName = serializedObject.FindProperty("CustomName"); + createFileCache = serializedObject.FindProperty("CreateFileCache"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.PropertyField(useCustomName); + if (useCustomName.boolValue) + { + EditorGUILayout.PropertyField(customName); + } + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta new file mode 100644 index 000000000..f8563100d --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Editor/RuntimeCacheManagerBehaviourEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f504b61d8125144a89d1eee4768fcb2b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs b/Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs similarity index 100% rename from Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs rename to Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs diff --git a/Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs.meta b/Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs.meta similarity index 100% rename from Runtime/Mapbox/Example/Editor/UnityContextDrawer.cs.meta rename to Runtime/Mapbox/BaseModule/Editor/UnityContextDrawer.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs b/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs index 4882b1adb..af7738e8e 100644 --- a/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs +++ b/Runtime/Mapbox/BaseModule/Map/IMapInformation.cs @@ -22,6 +22,7 @@ public interface IMapInformation event Action LatitudeLongitudeChanged; event Action WorldScaleChanged; Func QueryElevation { get; set; } + TerrainInfo Terrain { get; } float GetLatitudeCompensationForLocation { get; } float GetScaleFor(float zoomValue); } diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs new file mode 100644 index 000000000..7f581d52e --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs @@ -0,0 +1,18 @@ +namespace Mapbox.BaseModule.Map +{ + /// + /// Opt-in extension for + /// implementations that want to observe tile lifecycle events (load / unload) to + /// run their own bookkeeping. The observer side of the pattern; the subject side + /// is . + /// + /// calls + /// on every layer module that implements this interface, after the modules are + /// added but before their own Initialize runs. Modules that don't need to + /// observe tile lifecycle simply don't implement this interface. + /// + public interface ITileLifecycleObserver + { + void AttachToMapVisualizer(ITileLifecycleSource source); + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs.meta b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs.meta new file mode 100644 index 000000000..4a779949e --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleObserver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 518417b35d822432ab552cc4e5d26cfb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs new file mode 100644 index 000000000..3e403e09c --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Unity; + +namespace Mapbox.BaseModule.Map +{ + /// + /// Read-only "subject" surface of the tile-lifecycle observer pattern. Implemented + /// by ; consumed by + /// implementations (and helpers they own, e.g. terrain bounds tracking). + /// + /// Carrying only what an observer needs lets helpers depend on this narrow contract + /// instead of the full , which simplifies testing + /// and keeps modules from reaching into orchestration internals. + /// + public interface ITileLifecycleSource + { + IMapInformation MapInformation { get; } + + /// + /// Map tile finished loading with targeted detail level data. The tile is in + /// ActiveTiles by the time this fires. + /// + event Action TileLoaded; + + /// + /// Map tile is about to be pooled. Still in ActiveTiles when the event + /// fires; the visualizer removes it and calls Recycle immediately after. + /// Observers that need to read per-tile state (TerrainData, etc.) before it's + /// cleared should do so during this callback. + /// + event Action TileUnloading; + + IReadOnlyDictionary ActiveTiles { get; } + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta new file mode 100644 index 000000000..1e2821e70 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/ITileLifecycleSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c09e8843bdbf400abe18c42aa71d7374 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs b/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs index 69ead7537..f3631b3cd 100644 --- a/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs +++ b/Runtime/Mapbox/BaseModule/Map/ImageSourceSettings.cs @@ -12,8 +12,10 @@ public class ImageSourceSettings public bool UseRetinaTextures = true; [Tooltip("Non-readable textures halves the memory usage but cannot be queried for pixel values. Suitable if texture will be directly passed to shader.")] public bool UseNonReadableTextures = true; + + [Tooltip("Maximum tiles kept resident in the in-memory cache. Higher = fewer refetches at the cost of RAM. Rough budgets per tile: 256KB raster (512KB if UseRetinaTextures on) + 256KB float[] when CPU elevation extraction is on. At the default 100, terrain alone can sit at ~50MB resident. Default: 100.")] public int CacheSize = 100; - + [Tooltip("Maximum data level that'll be used for this module. Tiles can be higher zoom level but data will be lower level.")] public int ClampDataLevelToMax = 16; } diff --git a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs index eb0e0550e..2c5cf306e 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapInformation.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapInformation.cs @@ -48,10 +48,16 @@ public virtual void Initialize() public virtual void Initialize(LatitudeLongitude latitudeLongitude) { if(_isInitialized) return; - + SetLatitudeLongitude(latitudeLongitude); _isInitialized = true; QueryElevation = (tileId, x, y) => 0; + // Reset terrain bounds to TerrainInfo defaults so a previous map's Min/Max + // never bleeds into a fresh session. Field initializers already set defaults + // on new MapInformation instances, but explicitly resetting here guards + // against future paths that reuse an existing instance. + Terrain.MinElevation = TerrainInfo.DefaultMinElevation; + Terrain.MaxElevation = TerrainInfo.DefaultMaxElevation; } //PROPERTIES @@ -99,7 +105,7 @@ public virtual void SetInformation(LatitudeLongitude? latlng, float? zoom = null if(zoom.HasValue) Zoom = zoom.Value; if (pitch.HasValue) Pitch = pitch.Value; if (bearing.HasValue) Bearing = bearing.Value; - if (scale.HasValue) + if (scale.HasValue && !Mathf.Approximately(scale.Value, Scale)) { Scale = scale.Value; OnWorldScaleChanged(); @@ -117,6 +123,7 @@ public virtual void SetInformation(LatitudeLongitude? latlng, float? zoom = null } public Func QueryElevation { get; set; } + public TerrainInfo Terrain { get; } = new TerrainInfo(); public event Action SetView = (t) => {}; public event Action ViewChanged = (t) => {}; public event Action LatitudeLongitudeChanged = (t) => {}; diff --git a/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs b/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs index e3a6a2884..1f6f28e78 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapInformationStaticMethods.cs @@ -30,12 +30,21 @@ public static Vector3 ConvertLatLngToPositionForScale(this IMapInformation mapIn return scaledDeltaMercatorVector3; } - public static Vector3 ConvertLatLngToPosition(this IMapInformation mapInfo, LatitudeLongitude latlng) + public static Vector3 ConvertLatLngToPosition(this IMapInformation mapInfo, LatitudeLongitude latlng, bool queryElevation = false) { var mercator = Conversions.LatitudeLongitudeToWebMercator(latlng); var deltaMercator = mercator - mapInfo.CenterMercator; var scaledDeltaMercator = deltaMercator / mapInfo.Scale; - var scaledDeltaMercatorVector3 = scaledDeltaMercator.ToVector3xz(); + var elevation = 0f; + + if (queryElevation) + { + var tileId = Conversions.LatitudeLongitudeToTileId(latlng, 14).Canonical; + var pointPosition = Conversions.LatitudeLongitudeToInTile01(latlng, tileId); + elevation = mapInfo.QueryElevation(tileId, pointPosition.x, pointPosition.y); + } + + var scaledDeltaMercatorVector3 = new Vector3((float)scaledDeltaMercator.x, elevation, (float)scaledDeltaMercator.y); return scaledDeltaMercatorVector3; } diff --git a/Runtime/Mapbox/BaseModule/Map/MapService.cs b/Runtime/Mapbox/BaseModule/Map/MapService.cs index a73041675..35f4fb707 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapService.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapService.cs @@ -44,6 +44,11 @@ public bool IsReady() public bool GetTelemetryCollectionState() => _mapboxContext.GetTelemetryCollectiongState(); public virtual bool SetTelemetryCollectionState(bool state) => _mapboxContext.SetTelemetryCollectionState(state); + public virtual void ClearCachedData() + { + + } + public virtual void OnDestroy() { diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs b/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs index 2bb6fd435..bfaaf585d 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.IO; using Mapbox.BaseModule; using Mapbox.BaseModule.Telemetry; @@ -17,7 +18,11 @@ public class MapboxContext : IMapboxContext public MapboxContext() { - LoadConfiguration(); + } + + public IEnumerator Initialize() + { + yield return LoadConfigurationCoroutine(); } public string GetAccessToken() @@ -38,7 +43,7 @@ public MapboxTokenStatus TokenStatus() return _mapboxToken.Status; } - private void LoadConfiguration() + private MapboxConfiguration LoadAndParseConfig() { TextAsset configurationTextAsset = Resources.Load(Constants.Path.MAPBOX_RESOURCES_RELATIVE); if (null == configurationTextAsset) @@ -49,22 +54,58 @@ private void LoadConfiguration() var config = JsonUtility.FromJson(configurationTextAsset.text); config.Initialize(); - var tokenValidator = new MapboxTokenApi(); - tokenValidator.Retrieve(config.GetMapsSkuToken, config.AccessToken, (response) => + return config; + } + + private void HandleTokenResponse(MapboxConfiguration config, MapboxToken response) + { + _mapboxToken = response; + Configuration = config; + if (_mapboxToken.Status != MapboxTokenStatus.TokenValid) { - _mapboxToken = response; - if (_mapboxToken.Status != MapboxTokenStatus.TokenValid) + config.AccessToken = string.Empty; + Debug.LogError("Invalid Token"); + } + else + { + ConfigureTelemetry(); + } + } + + private const float TokenValidationTimeoutSeconds = 10f; + + public IEnumerator LoadConfigurationCoroutine(bool validateToken = true) + { + var config = LoadAndParseConfig(); + + if (validateToken) + { + var tokenValidator = new MapboxTokenApi(); + var configLoaded = false; + tokenValidator.Retrieve(config.GetMapsSkuToken, config.AccessToken, (response) => { - config.AccessToken = string.Empty; - Debug.LogError("Invalid Token"); - } - else + HandleTokenResponse(config, response); + configLoaded = true; + }); + + var elapsed = 0f; + while (!configLoaded) { - ConfigureTelemetry(); - } - }); + elapsed += Time.deltaTime; + if (elapsed >= TokenValidationTimeoutSeconds) + { + Debug.LogError("Token validation timed out. Proceeding with unvalidated configuration."); + Configuration = config; + break; + } - Configuration = config; + yield return null; + } + } + else + { + Configuration = config; + } } private void ConfigureTelemetry() diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs index 0317d4f29..82e3b4ae9 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMap.cs @@ -55,15 +55,12 @@ public IEnumerator Initialize() Initialized(); } - /// - /// Map update method, which we currently use per-frame, to recalculate the tile cover and run map visualizer on it. - /// - public void MapUpdated() + public void ForceRedraw() { MapService.TileCover(MapInformation, TileCover); MapVisualizer.Load(TileCover); } - + /// /// Provides a controlled method for jumping to specific locations on the map. /// Unlike standard frame-by-frame map updates, this method ensures precise @@ -133,7 +130,7 @@ public IEnumerator LoadMapViewCoroutine(LatitudeLongitude targetLocation, Action { Status = InitializationStatus.LoadingView; LoadViewStarting(); - MapInformation.SetInformation(targetLocation); + MapInformation.SetLatitudeLongitude(targetLocation); MapService.TileCover(MapInformation, TileCover); yield return MapVisualizer.LoadTileCoverToMemory(TileCover); @@ -157,9 +154,10 @@ public IEnumerator LoadTileCoverToMemory(TileCover cover) /// Zoom level of the map /// Pitch value of the camera /// Bearing value of the camera - public void ChangeView(LatitudeLongitude? latlng = null, float? zoom = null, float? pitch = null, float? bearing = null) + public void ChangeView(LatitudeLongitude? latlng = null, float? zoom = null, float? pitch = null, float? bearing = null, float? scale = null) { - MapInformation.SetInformation(latlng, zoom, pitch, bearing); + MapInformation.SetInformation(latlng, zoom, pitch, bearing, scale); + RedrawMap(); } public void OnDestroy() @@ -170,6 +168,17 @@ public void OnDestroy() public void UpdateTileCover() => MapService.TileCover(MapInformation, TileCover); + /// + /// Map redraw method, which we currently use on-demand, to recalculate the tile cover and run map visualizer on it. + /// + private void RedrawMap(IMapInformation mapInfo = null) + { + MapService.TileCover(mapInfo ?? MapInformation, TileCover); + MapVisualizer.Load(TileCover); + } + + + /// /// All modules are initialized, you can get&use any object and/or register to events safely. /// diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs new file mode 100644 index 000000000..44dfea495 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs @@ -0,0 +1,84 @@ +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; + +namespace Mapbox.BaseModule.Map +{ + public static class MapboxMapExtensions + { + public static bool TryGetMapTile(this MapboxMap map, UnwrappedTileId tileId, out UnityMapTile tile) + { + if (map.MapVisualizer.ActiveTiles.TryGetValue(tileId, out tile)) + { + return true; + } + + tile = null; + return false; + } + + public static bool TryGetMapTile(this MapboxMap map, LatitudeLongitude coordinates, out UnityMapTile tile, int maxZoomLevel = 20) + { + var maxTileId = Conversions.LatitudeLongitudeToTileId(coordinates, maxZoomLevel); + for (int i = maxZoomLevel; i > 1; i--) + { + if (map.MapVisualizer.ActiveTiles.TryGetValue(maxTileId, out tile)) + { + return true; + } + else + { + maxTileId = maxTileId.Parent; + } + } + + tile = null; + return false; + } + + /// + /// Only queries active tiles / visible world + /// + /// + /// + /// + /// + /// + /// + public static bool TryGetElevation(this MapboxMap map, LatitudeLongitude location, out float elevation, int maxZoomLevel = 20, int minZoomLevel = 2) + { + var maxTileId = Conversions.LatitudeLongitudeToTileId(location, maxZoomLevel); + UnityMapTile tile = null; + for (int i = maxZoomLevel; i >= minZoomLevel; i--) + { + if (map.MapVisualizer.ActiveTiles.TryGetValue(maxTileId, out tile)) + { + break; + } + else + { + maxTileId = maxTileId.Parent; + } + } + + // Check tile and nested properties for null to prevent NullReferenceException + if (tile != null && + tile.TerrainContainer != null && + tile.TerrainContainer.TerrainData != null) + { + var tilePos = Conversions.LatitudeLongitudeToInTile01(location, tile.TerrainContainer.TerrainData.TileId); + elevation = tile.TerrainContainer.QueryHeightData(tilePos.x, tilePos.y); + return true; + } + + elevation = float.MinValue; + return false; + } + + public static void ClearCachedData(this MapboxMap map) + { + map.MapService.ClearCachedData(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta new file mode 100644 index 000000000..bbdcfab69 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3d874d1186584faf95af2fa2761ddcbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs index 681dc1f4e..ec7db7875 100644 --- a/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs +++ b/Runtime/Mapbox/BaseModule/Map/MapboxMapVisualizer.cs @@ -14,27 +14,58 @@ namespace Mapbox.BaseModule.Map /// The primary object responsible for preparing the data and generating the visuals of the map. /// [Serializable] - public class MapboxMapVisualizer : IMapVisualizer + public class MapboxMapVisualizer : IMapVisualizer, ITileLifecycleSource { + // Upper bound of the Mercator tile pyramid. Used by DelveInto and any other + // recursion that needs an absolute hard cap independent of user-configurable + // MaximumZoomLevel settings (which represent LOD intent, not pyramid depth). + public const int MaxMercatorZoom = 22; + public List LayerModules; public Dictionary ActiveTiles { get; private set; } + public List TempTiles { get; private set; } + public IMapInformation MapInformation => _mapInformation; + + // Explicit interface impl narrows ActiveTiles to IReadOnlyDictionary for the + // ITileLifecycleSource contract while internal code keeps the mutable Dictionary + // it needs for Add/Remove. + IReadOnlyDictionary ITileLifecycleSource.ActiveTiles => ActiveTiles; + protected UnityContext _unityContext; protected IMapInformation _mapInformation; protected ITileCreator _tileCreator; private HashSet _toRemove; private HashSet _retainedTiles; - private int _tilePerFrameLimit = 20; - private int _tileCreatedThisFrame = 0; + private Coroutine _internalUpdateCoroutine; + private bool _destroyed; + + // Reusable scratch array to avoid per-frame allocation in InternalUpdateCoroutine + private readonly UnwrappedTileId[] _quadrants = new UnwrappedTileId[4]; + + // Explicit stack for the iterative DelveInto (was recursive with per-call new bool[4] + // and new UnwrappedTileId[4] allocations). Bounded by recursion depth × 4, so the + // List reaches its peak size after one large call and never re-allocates after. + private struct DelveFrame + { + public UnwrappedTileId TileId; + public int Depth; + public int Found; // bitmask: bit i set if quadrant i is satisfied (here or via descendants) + public int NextChild; // 0..4 + public int ParentFrame; // index in _delveStack, or -1 for root + public int ParentSlot; // which slot of the parent we are filling (0..3) + } + private readonly List _delveStack = new List(); public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityContext, ITileCreator tileCreator) { _unityContext = unityContext; _mapInformation = mapInformation; - + ActiveTiles = new Dictionary(100); + TempTiles = new List(); LayerModules = new List(); - + _tileCreator = tileCreator; _tileCreator.OnTileBroken += (tt) => { @@ -45,9 +76,12 @@ public MapboxMapVisualizer(IMapInformation mapInformation, UnityContext unityCon }; _mapInformation.WorldScaleChanged += RepositionAllTiles; - + _mapInformation.LatitudeLongitudeChanged += RepositionAllTiles; + _toRemove = new HashSet(); _retainedTiles = new HashSet(); + + _internalUpdateCoroutine = Runnable.Instance.StartCoroutine(InternalUpdate()); } public virtual IEnumerator Initialize() @@ -63,10 +97,21 @@ public virtual IEnumerator Initialize() var terrainStrategy = new FlatTerrainStrategy(); yield return _tileCreator.Initialize(terrainStrategy); } - + + // Give layer modules a chance to wire bookkeeping that observes tile lifecycle + // (terrain bounds tracker, etc) BEFORE their own Initialize runs — so any + // subscriptions are in place when the first tiles start loading. + foreach (var module in LayerModules) + { + if (module is ITileLifecycleObserver observer) + { + observer.AttachToMapVisualizer(this); + } + } + yield return LayerModules.Select(x => x.Initialize()).WaitForAll(); } - + /// /// Prepare data and visuals for given tile cover. It loads the data to memory, generates vector feature visuals /// and prepare it all to ensure following tile requests will be finished in single frame. @@ -80,80 +125,146 @@ public virtual IEnumerator LoadTileCoverToMemory(TileCover tileCover) var coroutines = LayerModules.SelectMany(x => x.GetTileCoverCoroutines(hashsetTiles).Where(x => x != null)); yield return coroutines.WaitForAll(); } - - /// - /// Create the map in given tileCover area. Makes decision to load or unload tiles and handle temporary filler - /// tiles until actual tiles are loaded. - /// - /// + public virtual void Load(TileCover tileCover) { - _tileCreatedThisFrame = 0; - _toRemove.Clear(); - _retainedTiles.Clear(); - - foreach (var tile in ActiveTiles.Values) + RemoveUnnecessaryTiles(tileCover); + + // Protect filler children of temp tiles that are still loading. + // Without this, fillers get pooled one frame after creation (DelveInto only + // rescues them on the frame the temp tile is first added). + for (var i = TempTiles.Count - 1; i >= 0; i--) { - _toRemove.Add(tile.UnwrappedTileId); + var tempTile = TempTiles[i]; + if (tempTile.Children != null && ActiveTiles.ContainsKey(tempTile.UnwrappedTileId)) + { + foreach (var child in tempTile.Children) + { + _toRemove.Remove(child.UnwrappedTileId); + } + } } foreach (var tileId in tileCover.Tiles) { - _retainedTiles.Add(tileId.Canonical); - UnityMapTile unityMapTile = null; - _toRemove.Remove(tileId); - - if (ActiveTiles.TryGetValue(tileId, out unityMapTile)) + if (ActiveTiles.ContainsKey(tileId)) { - if (unityMapTile.IsTemporary) - { - CreateTile(unityMapTile); - } - - ShowTile(unityMapTile); continue; } - if (_tileCreatedThisFrame < _tilePerFrameLimit) + UnityMapTile unityMapTile = null; { if (CreateTileInstant(tileId, out unityMapTile)) { ShowTile(unityMapTile); - _tileCreatedThisFrame++; continue; } else { - var coveredByQuadrants = DelveInto(tileId, recursiveDepth: 1); + CreateTempTile(tileId, out unityMapTile); + // Reuse the tile's own Children list across pool/reuse cycles instead of + // allocating one per cache miss. PoolTile clears it on return. + if (unityMapTile.Children == null) + unityMapTile.Children = new List(); + else + unityMapTile.Children.Clear(); + var coveredByQuadrants = DelveInto(tileId, unityMapTile.Children, recursiveDepth: 1); + ActiveTiles.Add(tileId, unityMapTile); + TempTiles.Add(unityMapTile); if (!coveredByQuadrants) { - CreateTempTile(tileId, out unityMapTile); ShowTile(unityMapTile); } } } } - + foreach (var tileId in _toRemove) { - //this tryget is unnecessary, just get it. it cannot not be there. if (ActiveTiles.TryGetValue(tileId, out var tile)) { - TileUnloading(tile); + if (tile.LoadingState == LoadingState.Temporary) + { + TempTiles.Remove(tile); + } + PoolTile(tile); } - else - { - Debug.LogError($"Could not find tile {tileId}"); - } } - + + _retainedTiles.Clear(); + foreach (var tile in tileCover.Tiles) + { + _retainedTiles.Add(tile.Canonical); + } + foreach (var visualization in LayerModules) { visualization.RetainTiles(_retainedTiles); } } + /// + /// Create the map in given tileCover area. Makes decision to load or unload tiles and handle temporary filler + /// tiles until actual tiles are loaded. + /// + public virtual void InternalUpdateCoroutine() + { + //finish temp tiles from tempTiles list + _toRemove.Clear(); + for (var index = TempTiles.Count - 1; index >= 0; index--) + { + var tilePair = TempTiles[index]; + if (!ActiveTiles.ContainsKey(tilePair.UnwrappedTileId)) + { + TempTiles.RemoveAt(index); + continue; + } + + if (tilePair.LoadingState == LoadingState.Temporary && CreateTile(tilePair)) + { + ShowTile(tilePair); + + if (tilePair.Children != null && tilePair.Children.Count > 0) + { + foreach (var child in tilePair.Children) + { + PoolTile(child); + } + tilePair.Children.Clear(); + } + + TempTiles.RemoveAt(index); + + _quadrants[0] = tilePair.UnwrappedTileId.Quadrant(0); + _quadrants[1] = tilePair.UnwrappedTileId.Quadrant(1); + _quadrants[2] = tilePair.UnwrappedTileId.Quadrant(2); + _quadrants[3] = tilePair.UnwrappedTileId.Quadrant(3); + for (int i = 0; i < 4; i++) + { + if (ActiveTiles.TryGetValue(_quadrants[i], out _)) + { + _toRemove.Add(_quadrants[i]); + } + } + } + } + + foreach (var tileId in _toRemove) + { + if (ActiveTiles.TryGetValue(tileId, out var tile)) + { + if (tile.LoadingState == LoadingState.Temporary) + { + TempTiles.Remove(tile); + } + + PoolTile(tile); + } + } + } + + /// /// Minimal function that'll try to load view with whatever data is available. /// It will not unload any tiles, it will not trigger any data fetching. @@ -166,16 +277,30 @@ public void LoadSnapshot(TileCover tileCover) { foreach (var tileId in tileCover.Tiles) { + if (ActiveTiles.ContainsKey(tileId)) + continue; + UnityMapTile unityMapTile = null; if (!CreateTileInstant(tileId, out unityMapTile)) //if we can't fully load the tile + { CreateTempTile(tileId, out unityMapTile); //we load it whatever data we can find - + ActiveTiles.Add(tileId, unityMapTile); + TempTiles.Add(unityMapTile); + } + ShowTile(unityMapTile); } } public void OnDestroy() { + _destroyed = true; + if (_internalUpdateCoroutine != null && Runnable.Instance != null) + { + Runnable.Instance.StopCoroutine(_internalUpdateCoroutine); + _internalUpdateCoroutine = null; + } + foreach (var layerModule in LayerModules) { layerModule.OnDestroy(); @@ -194,63 +319,149 @@ public void OnDestroy() public bool TryGetLayerModule(out T module) where T : ILayerModule { module = (T)LayerModules.FirstOrDefault(x => x is T); + if (module == null) + { + var composite = LayerModules.FirstOrDefault(x => x is CompositeLayerModule) as CompositeLayerModule; + if (composite != null) + { + module = (T)composite.LayerModules.FirstOrDefault(x => x is T); + } + } return module != null; } - - - - - protected bool DelveInto(UnwrappedTileId tileId, int recursiveDepth = 3) + + + private IEnumerator InternalUpdate() { - var quadrantCheck = new bool[4] { false, false, false, false }; - var quadrants = new UnwrappedTileId[4] + while (!_destroyed) { - tileId.Quadrant(0), - tileId.Quadrant(1), - tileId.Quadrant(2), - tileId.Quadrant(3), - }; - for (int i = 0; i < 4; i++) - { - var quadrant = quadrants[i]; - if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) - { - _toRemove.Remove(quadrant); - //_retainedTiles.Add(quadrant.Canonical); - ShowTile(unityMapTile); - quadrantCheck[i] = true; - } + InternalUpdateCoroutine(); + yield return null; } + } - if (recursiveDepth > 0) + private void RemoveUnnecessaryTiles(TileCover tileCover) + { + // Fillers used to be exempt from removal here, but that caused orphaned filler + // z-fighting when their logical parent transitioned out of TempTile state. They + // are now eligible for pooling; visual continuity is preserved by the temp-tile + // filler protection pass in Load() above. + _toRemove.Clear(); + foreach (var tilePair in ActiveTiles) { - for (int i = 0; i < 4; i++) + if (!tileCover.Tiles.Contains(tilePair.Key)) { - if (quadrantCheck[i] == false && tileId.Z < 22) - { - quadrantCheck[i] = DelveInto(quadrants[i], recursiveDepth - 1); - } + _toRemove.Add(tilePair.Key); } } + } + + + protected bool DelveInto(UnwrappedTileId tileId, List activeChildren, int recursiveDepth = 3) + { + // Iterative replacement for the prior recursive implementation. Same semantics: + // at each level we look up the 4 quadrants in ActiveTiles; missing ones recurse + // deeper up to `recursiveDepth`; if any quadrant at a level is satisfied (here + // or via a descendant), the remaining slots at THAT level get temp filler tiles. + _delveStack.Clear(); + _delveStack.Add(new DelveFrame + { + TileId = tileId, + Depth = recursiveDepth, + Found = 0, + NextChild = 0, + ParentFrame = -1, + ParentSlot = -1, + }); + + bool rootResult = false; - //if (quadrantCheck.Any(x => x)) - if (quadrantCheck[0] || quadrantCheck[1] || quadrantCheck[2] || quadrantCheck[3]) + while (_delveStack.Count > 0) { - for (int i = 0; i < 4; i++) + int frameIdx = _delveStack.Count - 1; + var frame = _delveStack[frameIdx]; + + if (frame.NextChild < 4) { - if (quadrantCheck[i] == false) + int slot = frame.NextChild; + var quadrant = frame.TileId.Quadrant(slot); + frame.NextChild++; + + if (ActiveTiles.TryGetValue(quadrant, out var unityMapTile)) { - CreateTempTile(quadrants[i], out var unityMapTile); - _mapInformation.PositionObjectFor(unityMapTile.gameObject, unityMapTile.CanonicalTileId); + _toRemove.Remove(quadrant); + unityMapTile.LoadingState = LoadingState.Filler; + activeChildren.Add(unityMapTile); + if (unityMapTile.Children != null && unityMapTile.Children.Count > 0) + { + foreach (var subchild in unityMapTile.Children) + { + activeChildren.Add(subchild); + } + unityMapTile.Children.Clear(); + } ShowTile(unityMapTile); - quadrantCheck[i] = true; + frame.Found |= 1 << slot; + _delveStack[frameIdx] = frame; + } + else if (frame.Depth > 0 && frame.TileId.Z < MaxMercatorZoom) + { + // Save the advanced NextChild before pushing the child frame — + // we'll resume this slot's evaluation after the child finishes. + _delveStack[frameIdx] = frame; + _delveStack.Add(new DelveFrame + { + TileId = quadrant, + Depth = frame.Depth - 1, + Found = 0, + NextChild = 0, + ParentFrame = frameIdx, + ParentSlot = slot, + }); + } + else + { + // Leaf miss: nothing found here, no deeper recursion allowed. + _delveStack[frameIdx] = frame; } } + else + { + bool anyFound = frame.Found != 0; + if (anyFound) + { + for (int i = 0; i < 4; i++) + { + if ((frame.Found & (1 << i)) == 0) + { + var q = frame.TileId.Quadrant(i); + CreateTempTile(q, out var unityMapTile); + unityMapTile.LoadingState = LoadingState.Filler; + ActiveTiles[q] = unityMapTile; + ShowTile(unityMapTile); + activeChildren.Add(unityMapTile); + } + } + } - return true; + if (frame.ParentFrame >= 0) + { + var parent = _delveStack[frame.ParentFrame]; + if (anyFound) + { + parent.Found |= 1 << frame.ParentSlot; + } + _delveStack[frame.ParentFrame] = parent; + } + else + { + rootResult = anyFound; + } + _delveStack.RemoveAt(frameIdx); + } } - return false; + return rootResult; } protected void ShowTile(UnityMapTile unityTile) @@ -258,13 +469,27 @@ protected void ShowTile(UnityMapTile unityTile) unityTile.gameObject.SetActive(true); _mapInformation.PositionObjectFor(unityTile.gameObject, unityTile.CanonicalTileId); } - + protected void PoolTile(UnityMapTile tile) { + if (tile.LoadingState == LoadingState.None) + return; + + TileUnloading(tile); ActiveTiles.Remove(tile.UnwrappedTileId); tile.Recycle(); - tile.IsTemporary = false; + tile.LoadingState = LoadingState.None; _tileCreator.PutTile(tile); + + if (tile.Children != null) + { + foreach (var tileChild in tile.Children) + { + PoolTile(tileChild); + } + + tile.Children.Clear(); + } } protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) @@ -276,34 +501,46 @@ protected void CreateTempTile(UnwrappedTileId tileId, out UnityMapTile tile) { module.LoadTempTile(tile); } - - tile.IsTemporary = true; - ActiveTiles.Add(tileId, tile); + + tile.LoadingState = LoadingState.Temporary; + // ActiveTiles.Add(tileId, tile); + // TempTiles.Add(tile); } - + protected bool CreateTileInstant(UnwrappedTileId tileId, out UnityMapTile tile) { - //we need to do positioning and scaling before mesh gen for now GetMapTile(tileId, out tile); var result = CreateTile(tile); - - //couldn't create the tile - if (!result) PoolTile(tile); + + if (!result) + { + tile.Recycle(); + tile.LoadingState = LoadingState.None; + _tileCreator.PutTile(tile); + } return result; } protected void GetMapTile(UnwrappedTileId tileId, out UnityMapTile tile) { - var rectd = Conversions.TileBoundsInUnitySpace(tileId, _mapInformation.CenterMercator, _mapInformation.Scale); + var rectd = Conversions.TileBoundsInUnitySpace(tileId, _mapInformation.CenterMercator, + _mapInformation.Scale); tile = null; tile = _tileCreator.GetTile(); - tile.transform.position = new Vector3((float) rectd.Center.x, 0, (float) rectd.Center.y); - tile.transform.localScale = Vector3.one * (float) rectd.Size.x; - tile.Initialize(tileId, (float) rectd.Size.x * _mapInformation.Scale); + // Local-space so the whole map composes with any parent transform on the + // BaseTileRoot / MapRoot hierarchy (translation + rotation). AR/MR setups + // typically parent MapRoot under an ARAnchor; setting world-space position + // here would silently reset that on every tile cover update. + // Note: non-unit MapRoot.localScale still breaks the quadtree LOD math + // (camera position is read in world space). Recommended pattern: drive + // map scale through MapInformation.Scale, not via MapRoot.transform. + tile.transform.localPosition = new Vector3((float)rectd.Center.x, 0, (float)rectd.Center.y); + tile.transform.localScale = Vector3.one * (float)rectd.Size.x; + tile.Initialize(tileId, (float)rectd.Size.x * _mapInformation.Scale); } - + protected bool CreateTile(UnityMapTile unityMapTile) { var tileFinished = true; @@ -316,11 +553,12 @@ protected bool CreateTile(UnityMapTile unityMapTile) if (tileFinished) { - unityMapTile.IsTemporary = false; + unityMapTile.LoadingState = LoadingState.Finished; if (!ActiveTiles.ContainsKey(unityMapTile.UnwrappedTileId)) { ActiveTiles.Add(unityMapTile.UnwrappedTileId, unityMapTile); } + TileLoaded(unityMapTile); } @@ -344,12 +582,13 @@ protected void RepositionAllTiles(IMapInformation mapInformation) module.UpdatePositioning(mapInformation); } } - + /// /// Map tile finished loading with targeted detail level data. This tile isn't temporary anymore, it'll be in /// ActiveTiles list. /// public event Action TileLoaded = (tile) => { }; + /// /// Map tile unloading event fires for tiles that are still in active tiles list but not in the latest tileCover. /// UnityMapTile object attached to event will be pooled after the event call. diff --git a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs new file mode 100644 index 000000000..c1a749ad7 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs @@ -0,0 +1,32 @@ +using System; + +namespace Mapbox.BaseModule.Map +{ + /// + /// Runtime terrain elevation information for the current map view. + /// Updated automatically as terrain tiles load and unload. + /// + [Serializable] + public class TerrainInfo + { + // Conservative defaults used on the first frame before any terrain tile is + // sampled, and again whenever ActiveTiles drops to zero. The MaxElevation + // floor keeps the tile-provider's bounds-height AABB tall enough to admit + // plausible mountain ranges (above Mt. Whitney) so steep terrain isn't + // frustum-culled on first paint. + public const float DefaultMinElevation = 0f; + public const float DefaultMaxElevation = 5000f; + + /// + /// Lowest observed elevation in meters across currently loaded terrain tiles. + /// Initialized to . + /// + public float MinElevation = DefaultMinElevation; + + /// + /// Highest observed elevation in meters across currently loaded terrain tiles. + /// Initialized to . + /// + public float MaxElevation = DefaultMaxElevation; + } +} diff --git a/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta new file mode 100644 index 000000000..a0385a724 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Map/TerrainInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5415e7100ee884a8f9e172b14483e543 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs index b1116d925..8eac6c607 100644 --- a/Runtime/Mapbox/BaseModule/Map/TileCreator.cs +++ b/Runtime/Mapbox/BaseModule/Map/TileCreator.cs @@ -28,15 +28,17 @@ public class TileCreator : ITileCreator private int _cacheSize; //private FlatTerrainStrategy _flatTerrainStrategy; private TerrainStrategy _terrainStrategy; - + private bool _instanceMaterial; + public UnityMapTile GetTile() => _tilePool.GetObject(); public void PutTile(UnityMapTile tile) => _tilePool.Put(tile); - public TileCreator(UnityContext unityContext, Material[] tileMaterials = null, int cacheSize = 25) + public TileCreator(UnityContext unityContext, Material[] tileMaterials = null, int cacheSize = 25, bool instanceMaterials = true) { TileMaterials = tileMaterials; _unityContext = unityContext; _cacheSize = cacheSize; + _instanceMaterial = instanceMaterials; } public IEnumerator Initialize(TerrainStrategy terrainStrategy = null) @@ -55,12 +57,25 @@ private UnityMapTile CreateTile(UnityContext unityContext) tile.transform.SetParent(_unityContext.BaseTileRoot, false); } - if (TileMaterials?.Length > 0) + if (TileMaterials?.Length == 1) { - tile.MeshRenderer.materials = TileMaterials; + // Always use sharedMaterial. Per-tile state goes through the tile's + // MaterialPropertyBlock (UnityTileTerrainContainer / UnityTileImageContainer + // mutate it). The win is avoiding a Material clone per renderer — not SRP + // Batcher batching (SetPropertyBlock disqualifies a renderer from the SRP + // Batcher) and not GPU instancing (off in the current materials/shaders). + // The legacy _instanceMaterial flag is retained on the constructor for + // back-compat but no longer differentiates: per-tile state lives on the + // property block, not on a unique Material instance. + tile.MeshRenderer.sharedMaterial = TileMaterials[0]; + tile.Material = tile.MeshRenderer.sharedMaterial; } - - tile.Material = tile.MeshRenderer.material; + else if (TileMaterials?.Length > 1) + { + tile.MeshRenderer.sharedMaterials = TileMaterials; + tile.Material = tile.MeshRenderer.sharedMaterial; + } + tile.gameObject.SetActive(false); _terrainStrategy?.RegisterTile(tile, false); tile.OnDataDisposed += OnTileBroken; diff --git a/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef b/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef index fa0fc9a47..6ebb83cd2 100644 --- a/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef +++ b/Runtime/Mapbox/BaseModule/MapboxBaseModule.asmdef @@ -2,7 +2,8 @@ "name": "MapboxBaseModule", "rootNamespace": "", "references": [ - "GUID:478a2357cc57436488a56e564b08d223" + "GUID:478a2357cc57436488a56e564b08d223", + "GUID:2665a8d13d1b3f18800f46e256720795" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm new file mode 100644 index 000000000..13caaa4dc --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm @@ -0,0 +1,217 @@ +#import +#import +#import +#import +#import +#import +#import "locationBridge.h" + +// Objective-C++ observer class +@interface LocationObserverBridge : NSObject +@property (nonatomic, assign) LocationUpdateCallback callback; +- (instancetype)initWithCallback:(LocationUpdateCallback)callback; +- (void)onLocationUpdateReceivedForLocations:(NSArray *)locations; +@end + +@implementation LocationObserverBridge + +- (instancetype)initWithCallback:(LocationUpdateCallback)callback { + self = [super init]; + if (self) { + _callback = callback; + } + return self; +} + +- (void)onLocationUpdateReceivedForLocations:(NSArray *)locations { + if (_callback && locations.count > 0) { + MBXLocation* loc = locations.firstObject; + + // Extract location data, handling optional NSNumber properties + double latitude = loc.latitude; + double longitude = loc.longitude; + float accuracy = loc.horizontalAccuracy ? [loc.horizontalAccuracy floatValue] : 0.0f; + // Convert timestamp from milliseconds to seconds + double timestamp = loc.timestamp / 1000.0; + double altitude = loc.altitude ? [loc.altitude doubleValue] : 0.0; + float speed = loc.speed ? [loc.speed floatValue] : 0.0f; + float bearing = loc.bearing ? [loc.bearing floatValue] : 0.0f; + + // Call the C# callback + _callback(latitude, longitude, accuracy, timestamp, altitude, speed, bearing); + } +} + +@end + +// Objective-C++ service observer class +@interface LocationServiceObserverBridge : NSObject +@property (nonatomic, assign) AuthorizationStatusCallback authCallback; +@property (nonatomic, assign) AccuracyAuthorizationCallback accuracyCallback; +@property (nonatomic, assign) AvailabilityCallback availabilityCallback; +- (instancetype)initWithCallbacks:(AuthorizationStatusCallback)authCallback + accuracyCallback:(AccuracyAuthorizationCallback)accuracyCallback + availabilityCallback:(AvailabilityCallback)availabilityCallback; +@end + +@implementation LocationServiceObserverBridge + +- (instancetype)initWithCallbacks:(AuthorizationStatusCallback)authCallback + accuracyCallback:(AccuracyAuthorizationCallback)accuracyCallback + availabilityCallback:(AvailabilityCallback)availabilityCallback { + self = [super init]; + if (self) { + _authCallback = authCallback; + _accuracyCallback = accuracyCallback; + _availabilityCallback = availabilityCallback; + } + return self; +} + +- (void)onPermissionStatusChangedForPermission:(MBXPermissionStatus)permission { + if (_authCallback) { + _authCallback((int)permission); + } +} + +- (void)onAccuracyAuthorizationChangedForAccuracyAuthorization:(MBXAccuracyAuthorization)accuracyAuthorization { + if (_accuracyCallback) { + _accuracyCallback((int)accuracyAuthorization); + } +} + +- (void)onAvailabilityChangedForIsAvailable:(BOOL)isAvailable { + if (_availabilityCallback) { + _availabilityCallback(isAvailable); + } +} + +@end + +// Global storage for observers and providers (must persist) +static NSMutableDictionary* observerMap = nil; +static NSMutableDictionary>* providerMap = nil; +static CLLocationManager* authLocationManager = nil; +static LocationServiceObserverBridge* serviceObserver = nil; + +// Implementation of extern "C" functions +void requestLocationAuthorization() +{ + if (!authLocationManager) { + authLocationManager = [[CLLocationManager alloc] init]; + } + + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + + if (status == kCLAuthorizationStatusNotDetermined) { + [authLocationManager requestWhenInUseAuthorization]; + } +} +void* startLocationUpdatesWithSettings( + long long minimumInterval, long long maximumInterval, long long interval, + int accuracyLevel, float displacement, + LocationUpdateCallback callback) +{ + // Check location authorization status + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + + if (!observerMap) { + observerMap = [NSMutableDictionary dictionary]; + } + if (!providerMap) { + providerMap = [NSMutableDictionary dictionary]; + } + + // Get location service + id service = [MBXLocationServiceFactory getOrCreate]; + + // Create interval settings + MBXIntervalSettings* intervalSettings = [[MBXIntervalSettings alloc] + initWithMinimumInterval:@(minimumInterval) + maximumInterval:@(maximumInterval) + interval:@(interval)]; + + // Create location provider request + MBXLocationProviderRequest* request = [[MBXLocationProviderRequest alloc] + initWithAccuracy:@(accuracyLevel) + displacement:@(displacement) + interval:intervalSettings]; + + // Get device location provider + auto result = [service getDeviceLocationProviderForRequest:request]; + + if (![result isValue]) { + NSLog(@"Failed to get device location provider: %@", result.error); + return NULL; + } + + id provider = result.value; + + // Create and register observer + LocationObserverBridge* observer = [[LocationObserverBridge alloc] initWithCallback:callback]; + + // Store both provider and observer + NSValue* providerKey = [NSValue valueWithPointer:(__bridge void*)provider]; + observerMap[providerKey] = observer; + providerMap[providerKey] = provider; + + // Request authorization if needed before starting location updates + if (status == kCLAuthorizationStatusNotDetermined) { + if (!authLocationManager) { + authLocationManager = [[CLLocationManager alloc] init]; + } + [authLocationManager requestWhenInUseAuthorization]; + } + + // Add observer to provider (this starts location updates) + [provider addLocationObserverForObserver:observer]; + + // Return provider pointer for later cleanup + return (__bridge_retained void*)provider; +} + +void stopLocationUpdates(void* providerPtr) { + if (providerPtr == NULL) { + return; + } + + id provider = (__bridge_transfer id)providerPtr; + NSValue* providerKey = [NSValue valueWithPointer:(__bridge void*)provider]; + LocationObserverBridge* observer = observerMap[providerKey]; + + if (observer) { + [provider removeLocationObserverForObserver:observer]; + [observerMap removeObjectForKey:providerKey]; + [providerMap removeObjectForKey:providerKey]; + } +} + +void addLocationServiceObserver( + AuthorizationStatusCallback authCallback, + AccuracyAuthorizationCallback accuracyCallback, + AvailabilityCallback availabilityCallback) +{ + // Remove existing observer if any + if (serviceObserver) { + removeLocationServiceObserver(); + } + + // Create new service observer + serviceObserver = [[LocationServiceObserverBridge alloc] + initWithCallbacks:authCallback + accuracyCallback:accuracyCallback + availabilityCallback:availabilityCallback]; + + // Get location service and add observer + id service = [MBXLocationServiceFactory getOrCreate]; + [service registerObserverForObserver:serviceObserver]; +} + +void removeLocationServiceObserver() +{ + if (serviceObserver) { + id service = [MBXLocationServiceFactory getOrCreate]; + [service unregisterObserverForObserver:serviceObserver]; + serviceObserver = nil; + } +} diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta new file mode 100644 index 000000000..3d4911964 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/LocationBridge.mm.meta @@ -0,0 +1,42 @@ +fileFormatVersion: 2 +guid: a0bfa1c7fd3274f8f8ebac9e34c9a72a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + VisionOS: VisionOS + second: + enabled: 1 + settings: {} + - first: + iPhone: iOS + second: + enabled: 1 + settings: {} + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h new file mode 100644 index 000000000..331db87f2 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h @@ -0,0 +1,37 @@ +#import +#import +#import + +extern "C" { + // Callback type for location updates + typedef void (*LocationUpdateCallback)( + double latitude, double longitude, + float accuracy, double timestamp, + double altitude, float speed, float bearing); + + // Callback types for service observer + typedef void (*AuthorizationStatusCallback)(int status); + typedef void (*AccuracyAuthorizationCallback)(int accuracy); + typedef void (*AvailabilityCallback)(bool available); + + // Request location permissions (call this before starting location updates) + void requestLocationAuthorization(); + + // Create location service and start updates + void* startLocationUpdatesWithSettings( + long long minimumInterval, long long maximumInterval, long long interval, + int accuracyLevel, float displacement, + LocationUpdateCallback callback); + + // Stop location updates + void stopLocationUpdates(void* providerPtr); + + // Add service observer for permission and availability changes + void addLocationServiceObserver( + AuthorizationStatusCallback authCallback, + AccuracyAuthorizationCallback accuracyCallback, + AvailabilityCallback availabilityCallback); + + // Remove service observer + void removeLocationServiceObserver(); +} diff --git a/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta new file mode 100644 index 000000000..e7b675c03 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Plugins/iOS/locationBridge.h.meta @@ -0,0 +1,81 @@ +fileFormatVersion: 2 +guid: d8ceb9c4682094861b083650966966e3 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + AndroidSharedLibraryType: Executable + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Plugins/link.xml b/Runtime/Mapbox/BaseModule/Plugins/link.xml index d316281b6..ba5a20d7c 100644 --- a/Runtime/Mapbox/BaseModule/Plugins/link.xml +++ b/Runtime/Mapbox/BaseModule/Plugins/link.xml @@ -1,4 +1,17 @@ + + + + + + + diff --git a/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs b/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs index 73e601786..a75c44c0b 100644 --- a/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs +++ b/Runtime/Mapbox/BaseModule/Telemetry/TelemetryAndroid.cs @@ -149,15 +149,6 @@ public void SendSdkEvent() public void SetLocationCollectionState(bool enable) { - if (enable) - { - Input.location.Start(); - } - else - { - Input.location.Stop(); - } - _telemetryUtilsClass.CallStatic(_setEventsCollectionStatMethodName, enable, null); } diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs index 137d17406..2bf00dfcd 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/DataFetchingManagerBehaviour.cs @@ -2,7 +2,7 @@ using Mapbox.BaseModule.Data.DataFetchers; using UnityEngine; -namespace Mapbox.Example.Scripts.ModuleBehaviours +namespace Mapbox.BaseModule.Unity.ModuleBehaviours { public class DataFetchingManagerBehaviour : MonoBehaviour { diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs index ede5f920f..6c08e08a5 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/MapboxCacheManagerBehaviour.cs @@ -1,42 +1,9 @@ using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; -using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; -using Mapbox.BaseModule.Unity; using UnityEngine; -namespace Mapbox.Example.Scripts.ModuleBehaviours +namespace Mapbox.BaseModule.Unity.ModuleBehaviours { - public class RuntimeCacheManagerBehaviour : MapboxCacheManagerBehaviour - { - public MapboxCacheManager CacheManager; - public MemoryCache MemoryCache; - - public MapboxCacheManager GetCacheManager() => CacheManager; - - public bool CreateSqliteCache = true; - public bool CreateFileCache = true; - - public override MapboxCacheManager GetCacheManager(UnityContext unityContext, DataFetchingManager dataFetchingManager) - { - if (CacheManager == null) - { - SqliteCache sqliteCache = null; - FileCache fileCache = null; - sqliteCache = CreateSqliteCache ? new SqliteCache(unityContext.TaskManager, 1000) : null; - fileCache = CreateFileCache ? new FileCache(unityContext.TaskManager) : null; - MemoryCache = new MemoryCache(); - - CacheManager = new MapboxCacheManager( - unityContext, - MemoryCache, - fileCache, - sqliteCache); - } - - return CacheManager; - } - } - public abstract class MapboxCacheManagerBehaviour : MonoBehaviour { public abstract MapboxCacheManager GetCacheManager(UnityContext unityContext, DataFetchingManager dataFetchingManager); diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs new file mode 100644 index 000000000..f0f478529 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs @@ -0,0 +1,62 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; + +namespace Mapbox.BaseModule.Unity.ModuleBehaviours +{ + public class RuntimeCacheManagerBehaviour : MapboxCacheManagerBehaviour + { + public MapboxCacheManager CacheManager; + public MemoryCache MemoryCache; + + public MapboxCacheManager GetCacheManager() => CacheManager; + + public bool CreateSqliteCache = true; + + public bool CreateFileCache = true; + + public bool UseCustomName = false; + public string CustomName; + + public override MapboxCacheManager GetCacheManager(UnityContext unityContext, DataFetchingManager dataFetchingManager) + { + if (CacheManager == null) + { + SqliteCache sqliteCache = null; + FileCache fileCache = null; + if (CreateSqliteCache) + { + if (UseCustomName) + { + sqliteCache = new SqliteCache(unityContext.TaskManager, 1000, CustomName); + } + else + { + sqliteCache = new SqliteCache(unityContext.TaskManager, 1000); + } + } + + if (CreateFileCache) + { + if (UseCustomName) + { + fileCache = new FileCache(unityContext.TaskManager, CustomName); + } + else + { + fileCache = new FileCache(unityContext.TaskManager); + } + } + + MemoryCache = new MemoryCache(); + CacheManager = new MapboxCacheManager( + unityContext, + MemoryCache, + fileCache, + sqliteCache); + } + + return CacheManager; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta new file mode 100644 index 000000000..856152900 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/RuntimeCacheManagerBehaviour.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 459c81193c94429ba939258f8a66103f +timeCreated: 1768224967 \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs index d3ebaed22..d13a1f432 100644 --- a/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs +++ b/Runtime/Mapbox/BaseModule/Unity/ModuleBehaviours/TileCreatorBehaviour.cs @@ -1,12 +1,8 @@ using System; -using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Unity; -using Mapbox.ImageModule.Terrain.Settings; -using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; -namespace Mapbox.Example.Scripts.ModuleBehaviours +namespace Mapbox.BaseModule.Unity.ModuleBehaviours { public class TileCreatorBehaviour : MonoBehaviour { @@ -15,13 +11,14 @@ public class TileCreatorBehaviour : MonoBehaviour [Tooltip("Materials for base map tile mesh and gameobject")] public Material[] TileMaterials; + public bool InstanceMaterials = true; public int CacheSize = 25; public ITileCreator GetTileCreator(UnityContext unityContext) { if (_tileCreator != null) return _tileCreator; - _tileCreator = new TileCreator(unityContext, TileMaterials, CacheSize); + _tileCreator = new TileCreator(unityContext, TileMaterials, CacheSize, InstanceMaterials); return _tileCreator; } } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs b/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs index cc0023808..0bca4629d 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityContext.cs @@ -1,47 +1,204 @@ using System; +using System.Collections; using Mapbox.BaseModule.Data.Tasks; using UnityEngine; +using UnityEngine.Android; namespace Mapbox.BaseModule.Unity { [Serializable] public class UnityContext { + public Action LocationPermissionStateChanged; + public TaskManager TaskManager; + public LocationPermissionState LocationPermissionState = LocationPermissionState.Waiting; [NonSerialized] public MonoBehaviour CoroutineStarter; + [Tooltip("Root object to hold all map related game objects")] public Transform MapRoot; + [Tooltip("Root object for all tile objects which created the base map")] public Transform BaseTileRoot; + [Tooltip("Root object for all runtime generated visuals. Mainly the vector feature visuals.")] public Transform RuntimeGenerationRoot; - public UnityContext() - { - //TaskManager = new TaskManager(); - } - - public void Initialize(TaskManager providedTaskManager = null) + + private LocationPermissionHandler _locationPermissionHandler = new(); + + public IEnumerator Initialize(TaskManager providedTaskManager = null) { if (TaskManager == null) { TaskManager = providedTaskManager ?? new TaskManager(); } - + TaskManager.Initialize(); + // worldPositionStays: false on the SetParent calls below. Without that flag + // (default true), Unity preserves world transform by writing to localRotation — + // so if MapRoot is rotated when Initialize runs, auto-created roots end up + // with localRotation = inverse(MapRoot.rotation), counter-rotating any + // children they hold. Explicit localRotation = identity afterwards is + // defensive in case the scene-authored root has a non-default rotation we + // want to ignore. BaseTileRoot = BaseTileRoot == null ? new GameObject("BaseTiles").transform : BaseTileRoot; - BaseTileRoot.SetParent(MapRoot); + BaseTileRoot.SetParent(MapRoot, worldPositionStays: false); BaseTileRoot.transform.localPosition = Vector3.zero; + BaseTileRoot.transform.localRotation = Quaternion.identity; - RuntimeGenerationRoot = RuntimeGenerationRoot == null ? new GameObject("RuntimeObjectsRoot").transform : RuntimeGenerationRoot; - RuntimeGenerationRoot.SetParent(MapRoot); + RuntimeGenerationRoot = RuntimeGenerationRoot == null + ? new GameObject("RuntimeObjectsRoot").transform + : RuntimeGenerationRoot; + RuntimeGenerationRoot.SetParent(MapRoot, worldPositionStays: false); RuntimeGenerationRoot.transform.localPosition = Vector3.zero; + RuntimeGenerationRoot.transform.localRotation = Quaternion.identity; + yield return null; } public void OnDestroy() { TaskManager.OnDestroy(); } + + public IEnumerator HandlePermission() + { + yield return _locationPermissionHandler.HandlePermission(); + LocationPermissionState = _locationPermissionHandler.State; + LocationPermissionStateChanged?.Invoke(); + } + } + + public enum LocationPermissionState + { + Waiting, + Granted, + Denied, + DeniedPermanently + } + + public class LocationPermissionHandler + { + public LocationPermissionState State = LocationPermissionState.Waiting; + + public IEnumerator HandlePermission() + { + if (Permission.HasUserAuthorizedPermission(Permission.FineLocation)) + { + State = LocationPermissionState.Granted; + yield break; + } + +#if UNITY_ANDROID && !UNITY_EDITOR + yield return RequestLocationPermissionIfNeeded(); +#elif UNITY_IOS && !UNITY_EDITOR + if (!Input.location.isEnabledByUser) + { + yield return iOSAskPermission(); + } +#elif UNITY_EDITOR + // Editor / non-Android: assume granted + OnPermissionGranted(); +#endif + } + +#if UNITY_IOS && !UNITY_EDITOR + public IEnumerator iOSAskPermission() + { + Input.location.Start(); + + int waitTime = 10; + while (Input.location.status == LocationServiceStatus.Initializing && waitTime > 0) + { + yield return new WaitForSeconds(1); + waitTime--; + } + + if (waitTime <= 0) + { + Debug.LogWarning("Location service init timeout"); + OnPermissionDenied(); + yield break; + } + + if (Input.location.status == LocationServiceStatus.Failed) + { + Debug.LogWarning("Location service failed (likely denied)"); + OnPermissionDenied(); + yield break; + } + + OnPermissionGranted(); + } +#endif + +#if UNITY_ANDROID + private IEnumerator RequestLocationPermissionIfNeeded() + { + var permissionDone = false; + var callbacks = new PermissionCallbacks(); + callbacks.PermissionGranted += permission => + { + if (permission == Permission.FineLocation) + { + permissionDone = true; + OnPermissionGranted(); + } + }; + + callbacks.PermissionDenied += permission => + { + + if (permission == Permission.FineLocation) + { + permissionDone = true; + OnPermissionDenied(); + } + }; + + callbacks.PermissionDeniedAndDontAskAgain += permission => + { + if (permission == Permission.FineLocation) + { + permissionDone = true; + OnPermissionDeniedPermanently(); + } + }; + Permission.RequestUserPermission(Permission.FineLocation, callbacks); + + float elapsed = 0f; + const float timeout = 60f; + while (!permissionDone) + { + elapsed += Time.unscaledDeltaTime; + if (elapsed >= timeout) + { + Debug.LogWarning("Location permission request timed out — no callback received."); + OnPermissionDenied(); + yield break; + } + yield return null; + } + } +#endif + + private void OnPermissionGranted() + { + State = LocationPermissionState.Granted; + Debug.Log("Location permission GRANTED"); + } + + private void OnPermissionDenied() + { + State = LocationPermissionState.Denied; + Debug.Log("Location permission denied"); + } + + private void OnPermissionDeniedPermanently() + { + State = LocationPermissionState.DeniedPermanently; + Debug.LogWarning("Location permission denied permanently. User must enable location in system settings."); + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs index 1d13fb7b7..b4c57f594 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityMapTile.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using Mapbox.BaseModule.Data.Tiles; using Mapbox.BaseModule.Utilities; +using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; using UnityEngine.Rendering; @@ -19,7 +21,7 @@ public class UnityMapTile : MonoBehaviour public UnityTileImageContainer ImageContainer; public UnityTileVectorContainer VectorContainer; - private string _tileScaleFieldNameID = "_TileScale"; + private static readonly int TileScalePropertyId = Shader.PropertyToID("_TileScale"); private MeshRenderer _meshRenderer; public MeshRenderer MeshRenderer => _meshRenderer; @@ -27,9 +29,48 @@ public class UnityMapTile : MonoBehaviour public Material Material; private MeshFilter _meshFilter; public MeshFilter MeshFilter => _meshFilter; + public List Children; + + // Per-tile state (height texture, scale-offset, elevation multiplier, etc.) is + // pushed via this MaterialPropertyBlock instead of a per-tile Material instance. + // One Material is shared across the entire tile pool (set in TileCreator); the + // win is avoiding a Material clone per renderer (reading Renderer.material clones, + // SetPropertyBlock does not). This pattern is NOT SRP-Batcher-compatible — any + // SetPropertyBlock on a renderer excludes it from the SRP Batcher. It is also not + // GPU-instanced today (materials have m_EnableInstancingVariants:0 and the shader + // has no UNITY_INSTANCING_BUFFER block); see project_terrain_instancing for what + // a real batching rework would entail. + private MaterialPropertyBlock _propertyBlock; + public MaterialPropertyBlock PropertyBlock + { + get + { + if (_propertyBlock == null) + { + _propertyBlock = new MaterialPropertyBlock(); + } + return _propertyBlock; + } + } + + /// + /// Pushes the cached onto the renderer. Call once + /// after a logical batch of property mutations (the block can hold many properties + /// at once, so multiple Set* calls between two Apply calls coalesce). + /// + public void ApplyPropertyBlock() + { + if (_meshRenderer != null && _propertyBlock != null) + { + _meshRenderer.SetPropertyBlock(_propertyBlock); + } + } + public int MeshVertexCount = 0; - public bool IsTemporary = false; + //public bool IsTemporary = false; + + public LoadingState LoadingState; public void Awake() { @@ -50,20 +91,48 @@ public void Initialize(UnwrappedTileId tileId, float scale) #if UNITY_EDITOR gameObject.name = tileId.ToString(); #endif - - Material.SetFloat(_tileScaleFieldNameID, TileScale); + + PropertyBlock.SetFloat(TileScalePropertyId, TileScale); + ApplyPropertyBlock(); } public void ElevationUpdatedCallback() { - if (MeshFilter != null) + if (_meshRenderer != null) { var centerHeight = (TerrainContainer.TerrainData.MaxElevation + TerrainContainer.TerrainData.MinElevation) / 2 * TileScale; var boxHeight = (TerrainContainer.TerrainData.MaxElevation - TerrainContainer.TerrainData.MinElevation) * TileScale; - _meshFilter.mesh.bounds = new Bounds(new Vector3(.5f, centerHeight, -.5f), new Vector3(1, boxHeight, 1)); + // Renderer.localBounds overrides the Mesh's bounds for culling without + // cloning the Mesh. Using MeshFilter.mesh.bounds would silently clone the + // sharedMesh, which defeats per-tile Mesh sharing. + _meshRenderer.localBounds = new Bounds(new Vector3(.5f, centerHeight, -.5f), new Vector3(1, boxHeight, 1)); } OnElevationValuesUpdated(this); } + + // Hard cap used for fallback mesh bounds. Earth's highest point is ~8849m; 10000m + // gives a safe margin. Oversize bounds only widen the frustum test; they don't add + // any draw cost since there is no geometry outside the real elevation range. + private const float FallbackMaxElevationMeters = 10000f; + + /// + /// Applies a conservative mesh bounds that covers the full plausible terrain height + /// range so shader-displaced vertices stay inside the mesh's frustum culling volume + /// before real MinElevation/MaxElevation are known (or permanently, + /// when CPU extraction is disabled). A later call to + /// tightens the bounds if CPU elevation + /// eventually arrives. + /// + public void SetFallbackMeshBounds() + { + if (_meshRenderer == null) + { + return; + } + var boxHeight = FallbackMaxElevationMeters * TileScale; + var centerHeight = boxHeight * 0.5f; + _meshRenderer.localBounds = new Bounds(new Vector3(.5f, centerHeight, -.5f), new Vector3(1, boxHeight, 1)); + } public void Recycle() { @@ -83,6 +152,41 @@ private void OnDestroy() ImageContainer.OnDestroy(); TerrainContainer.OnDestroy(); VectorContainer.OnDestroy(); + + // Unity does not auto-free runtime-created Meshes when the GameObject is + // destroyed. Release the render mesh allocated in Awake and any dedicated + // collider mesh allocated by ElevatedTerrainStrategy. Skip the shader-mode + // shared flat mesh — it is owned by the strategy and disposed via + // TerrainLayerModule.OnDestroy. The identity checks avoid double-freeing + // an aliased render/collider mesh (FlatTerrainStrategy). Colliders may live + // on a dedicated-layer child GameObject; scan children too. Names are pulled + // from the strategy's constants so renames stay in sync. + var renderMesh = _meshFilter != null ? _meshFilter.sharedMesh : null; + var colliders = GetComponentsInChildren(includeInactive: true); + foreach (var mc in colliders) + { + if (mc.sharedMesh != null && + mc.sharedMesh.name == ElevatedTerrainStrategy.TerrainColliderMeshName && + mc.sharedMesh != renderMesh) + { + Destroy(mc.sharedMesh); + } + } + // Destroy the render mesh if it's a per-tile instance. Skip the strategy-owned + // shared flat mesh (those are destroyed centrally in the strategy's OnDestroy). + // Both the unnamed Awake mesh and named per-tile CPU meshes get freed here. + if (renderMesh != null && renderMesh.name != ElevatedTerrainStrategy.SharedFlatMeshName) + { + Destroy(renderMesh); + } } } + + public enum LoadingState + { + None, + Temporary, + Finished, + Filler + } } diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs index 818b6bd08..9a1b71499 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileImageContainer.cs @@ -30,7 +30,10 @@ public UnityTileImageContainer(UnityMapTile unityMapTile, Action onDispose) public void SetImageData(RasterData imageData, TileContainerState state = TileContainerState.Final) { - ImageData?.SetDisposeCallback(null); + if (ImageData != null) + { + ImageData.RemoveDisposeCallback(_onDispose); + } State = state; if (imageData.Texture == null || imageData.TileId.Z == 0) @@ -39,7 +42,7 @@ public void SetImageData(RasterData imageData, TileContainerState state = TileCo } ImageData = imageData; - ImageData.SetDisposeCallback(_onDispose); + ImageData.AddDisposeCallback(_onDispose); OnImageryUpdated(); } @@ -50,9 +53,11 @@ public void OnImageryUpdated() var scaleOffset = _unityMapTile.CanonicalTileId.CalculateScaleOffsetAtZoom(ImageData.TileId.Z); - _unityMapTile.Material.SetTexture(MainTex, ImageData.Texture); - _unityMapTile.Material.SetVector(MainTexSt, scaleOffset); - _unityMapTile.Material.SetFloat(MainTextureChangeTime, Time.time); + var block = _unityMapTile.PropertyBlock; + block.SetTexture(MainTex, ImageData.Texture); + block.SetVector(MainTexSt, scaleOffset); + block.SetFloat(MainTextureChangeTime, Time.time); + _unityMapTile.ApplyPropertyBlock(); } public RasterData GetAndClearImageData() @@ -60,9 +65,10 @@ public RasterData GetAndClearImageData() if (ImageData == null) return null; - _unityMapTile.Material.SetTexture(MainTex, Texture2D.blackTexture); + _unityMapTile.PropertyBlock.SetTexture(MainTex, Texture2D.blackTexture); + _unityMapTile.ApplyPropertyBlock(); var rd = ImageData; - ImageData.SetDisposeCallback(null); + ImageData.RemoveDisposeCallback(_onDispose); ImageData = null; return rd; } @@ -70,12 +76,21 @@ public RasterData GetAndClearImageData() public void DisableImagery() { State = TileContainerState.Final; - _unityMapTile.Material.SetTexture(MainTex, null); + _unityMapTile.PropertyBlock.SetTexture(MainTex, null); + _unityMapTile.ApplyPropertyBlock(); } public void OnDestroy() { - //anything to finalize here? + // Symmetric detach to mirror UnityTileTerrainContainer's fix: the multicast + // _onDispose callback we added at SetImageData must be removed here, otherwise + // a future RasterData eviction fires the closure into a destroyed tile and + // cascades through OnDataDisposed → OnTileBroken → PoolTile on a dead tile. + if (ImageData != null) + { + ImageData.RemoveDisposeCallback(_onDispose); + ImageData = null; + } } } } \ No newline at end of file diff --git a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs index c247f4ae7..d24af4fb4 100644 --- a/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs +++ b/Runtime/Mapbox/BaseModule/Unity/UnityTileTerrainContainer.cs @@ -28,6 +28,14 @@ public class UnityTileTerrainContainer [SerializeField] public TerrainData TerrainData; private Vector4 _terrainTextureScaleOffset; + /// + /// Scale/offset Vector4 that maps this tile's UV [0,1] to the shared data tile's + /// UV sub-region (data tile is currentZ − 2 zoom coarser than this render + /// tile). Exposed so the collider builder can lift sampling math out of its inner + /// loop instead of calling per vertex. + /// + public Vector4 TerrainTextureScaleOffset => _terrainTextureScaleOffset; + public UnityTileTerrainContainer(UnityMapTile unityMapTile, Action elevationUpdatedCallback, Action onDisposeCallback) { _unityMapTile = unityMapTile; @@ -35,28 +43,56 @@ public UnityTileTerrainContainer(UnityMapTile unityMapTile, Action elevationUpda ElevationValuesUpdated = elevationUpdatedCallback; } + /// + /// Assigns terrain data to this tile: wires the material height texture, applies a + /// conservative fallback mesh bounds (so shader-displaced verts don't get frustum- + /// culled before real Min/MaxElevation arrive), and subscribes to future elevation + /// updates. + /// + /// Raster + optional decoded elevation for this tile. + /// When true, the material's _ElevationMultiplier is set to 1 so the vertex shader samples _HeightTexture; otherwise 0 (CPU-displaced verts). + /// Marks the tile container as temporary (using a parent's data while its own loads) or final. public void SetTerrainData(TerrainData terrainData, bool useShaderElevation, TileContainerState state = TileContainerState.Final) { - terrainData?.SetDisposeCallback(null); - - State = state; - if (terrainData.Texture == null || terrainData.TileId.Z == 0) + // Detach from the previous TerrainData unconditionally — null-callers + // (deliberately or accidentally clearing the slot) would otherwise leak + // the prior subscriptions on the shared TerrainData. + if (TerrainData != null) + { + TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; + TerrainData.RemoveDisposeCallback(_onDisposeCallback); + } + + if (terrainData == null) { - Debug.Log("no texture?"); + TerrainData = null; + return; } + + State = state; TerrainData = terrainData; - TerrainData.SetDisposeCallback(_onDisposeCallback); - + // Add (don't replace) — multiple render tiles share one TerrainData. + TerrainData.AddDisposeCallback(_onDisposeCallback); + OnTerrainUpdated(); + + // Apply a generous fallback bounds up front so shader-displaced geometry does not + // get frustum-culled before real Min/MaxElevation arrive (or forever, if CPU + // elevation extraction is disabled). OnElevationValuesUpdated later tightens the + // bounds to the actual elevation range if the float[] is decoded. + _unityMapTile.SetFallbackMeshBounds(); + + // Subscribe multicast-safe so other listeners (e.g. ElevatedTerrainStrategy's + // deferred collider rebuild) can coexist. + TerrainData.ElevationValuesUpdated += OnElevationValuesUpdated; + if (TerrainData.IsElevationDataReady) { OnElevationValuesUpdated(); } - TerrainData.SetElevationChangedCallback(OnElevationValuesUpdated); - //TerrainData.ElevationValuesUpdated += OnElevationValuesUpdated; - - _unityMapTile.Material.SetFloat(ElevationMultiplier, useShaderElevation ? 1 : 0); + _unityMapTile.PropertyBlock.SetFloat(ElevationMultiplier, useShaderElevation ? 1 : 0); + _unityMapTile.ApplyPropertyBlock(); } public void OnTerrainUpdated() @@ -65,22 +101,20 @@ public void OnTerrainUpdated() return; _terrainTextureScaleOffset = _unityMapTile.CanonicalTileId.CalculateScaleOffsetAtZoom(TerrainData.TileId.Z); - - _unityMapTile.Material.SetVector(HeightTextureST, _terrainTextureScaleOffset); - _unityMapTile.Material.SetTexture(HeightTexture, TerrainData.Texture); - //_unityMapTile._material.SetFloat(_tileScaleFieldNameID, _unityMapTile.TileScale); - _unityMapTile.Material.SetFloat("_IsFallbackTexture", 0); - _unityMapTile.Material.SetFloat(ElevationChangeTime, Time.time); + var block = _unityMapTile.PropertyBlock; + block.SetVector(HeightTextureST, _terrainTextureScaleOffset); + block.SetTexture(HeightTexture, TerrainData.Texture); + block.SetFloat("_IsFallbackTexture", 0); + block.SetFloat(ElevationChangeTime, Time.time); + _unityMapTile.ApplyPropertyBlock(); } public void OnElevationValuesUpdated() { - if (TerrainData == null) - { - Debug.Log("TerrainData is null, missing a isRecycled check?"); - return; - } + // Multicast dispose callback can fire on a container whose TerrainData was + // already cleared (concurrent eviction during readback). Bail silently. + if (TerrainData == null) return; TerrainData.IsElevationDataReady = true; ElevationValuesUpdated(); } @@ -91,7 +125,9 @@ public TerrainData GetAndClearTerrainData() return null; TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; - _unityMapTile.Material.SetTexture(HeightTexture, Texture2D.grayTexture); + TerrainData.RemoveDisposeCallback(_onDisposeCallback); + _unityMapTile.PropertyBlock.SetTexture(HeightTexture, Texture2D.grayTexture); + _unityMapTile.ApplyPropertyBlock(); var rd = TerrainData; TerrainData = null; return rd; @@ -99,34 +135,62 @@ public TerrainData GetAndClearTerrainData() public float QueryHeightData(float x, float y) { - if (TerrainData != null && TerrainData.ElevationValues.Length > 0) + var values = TerrainData?.ElevationValues; + if (values == null || values.Length == 0) { - var width = (int)Mathf.Sqrt(TerrainData.ElevationValues.Length); - var sectionWidth = width * _terrainTextureScaleOffset.x - 1; - var padding = width * new Vector2(_terrainTextureScaleOffset.z, _terrainTextureScaleOffset.w); - - var xx = padding.x + (x * sectionWidth); - var yy = padding.y + (y * sectionWidth); - - var index = (int) yy * width - + (int) xx; - if (TerrainData.ElevationValues.Length <= index) - { - return 0; - } - else - { - return TerrainData.ElevationValues[(int) yy * width + (int) xx]; - } - + return 0; } - return 0; + + var width = (int)Mathf.Sqrt(values.Length); + var sectionWidth = width * _terrainTextureScaleOffset.x - 1f; + var xx = _terrainTextureScaleOffset.z * width + x * sectionWidth; + var yy = _terrainTextureScaleOffset.w * width + y * sectionWidth; + return BilinearSampleElevation(values, width, xx, yy); + } + + /// + /// Bilinearly samples the decoded elevation array at (xx, yy) in data-texture + /// pixel space. Render tiles share a coarser data tile with 16 neighbors, so a + /// 129-vertex mesh routinely samples a ~64×64 data sub-region — multiple verts + /// land inside the same texel. Nearest-neighbor sampling in that regime produces + /// visible stepping; the four-corner lerp smooths it out for ~4 reads + 3 lerps + /// per sample. + /// + private static float BilinearSampleElevation(float[] values, int width, float xx, float yy) + { + if (xx < 0f) xx = 0f; else if (xx > width - 1) xx = width - 1; + if (yy < 0f) yy = 0f; else if (yy > width - 1) yy = width - 1; + + var x0 = (int)xx; + var y0 = (int)yy; + var x1 = x0 + 1; if (x1 >= width) x1 = width - 1; + var y1 = y0 + 1; if (y1 >= width) y1 = width - 1; + var fx = xx - x0; + var fy = yy - y0; + + var row0 = y0 * width; + var row1 = y1 * width; + var h00 = values[row0 + x0]; + var h10 = values[row0 + x1]; + var h01 = values[row1 + x0]; + var h11 = values[row1 + x1]; + var h0 = h00 + (h10 - h00) * fx; + var h1 = h01 + (h11 - h01) * fx; + return h0 + (h1 - h0) * fy; } + /// + /// Hard-disable terrain for this tile: zeroes the shader elevation multiplier, + /// unsubscribes from , and drops the + /// cached reference. Call when the tile's zoom falls + /// outside the supported range so nothing lingers on it. + /// public void DisableTerrain() { State = TileContainerState.Final; - _unityMapTile.Material.SetFloat(ElevationMultiplier, 0); + GetAndClearTerrainData(); + _unityMapTile.PropertyBlock.SetFloat(ElevationMultiplier, 0); + _unityMapTile.ApplyPropertyBlock(); } public void OnDestroy() @@ -134,6 +198,11 @@ public void OnDestroy() if (TerrainData != null) { TerrainData.ElevationValuesUpdated -= OnElevationValuesUpdated; + // Symmetric detach: previously only the ElevationValuesUpdated handler was + // removed, but the multicast dispose callback stayed wired. On a later + // eviction of shared TerrainData, that delegate would fire into a destroyed + // tile — exactly the class of bug the multicast change was meant to fix. + TerrainData.RemoveDisposeCallback(_onDisposeCallback); TerrainData = null; } } diff --git a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph index 6458a0e0e..623b18e99 100644 --- a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph +++ b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/ElevatedTerrainShader.shadergraph @@ -155,12 +155,6 @@ }, { "m_Id": "60b082478380496b992d5fa1caa35112" - }, - { - "m_Id": "7cb37beeca794a5b89fb91fdcb9568fc" - }, - { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" } ], "m_GroupDatas": [ @@ -215,20 +209,6 @@ "m_SlotId": 0 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "0842a57d9c2a43cba116748f9622c197" - }, - "m_SlotId": 0 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "7cb37beeca794a5b89fb91fdcb9568fc" - }, - "m_SlotId": 1 - } - }, { "m_OutputSlot": { "m_Node": { @@ -285,20 +265,6 @@ "m_SlotId": 1 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" - }, - "m_SlotId": 3 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "293d0108d9584aa484fde050460d9f45" - }, - "m_SlotId": 0 - } - }, { "m_OutputSlot": { "m_Node": { @@ -495,20 +461,6 @@ "m_SlotId": 2 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "7cb37beeca794a5b89fb91fdcb9568fc" - }, - "m_SlotId": 2 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" - }, - "m_SlotId": 0 - } - }, { "m_OutputSlot": { "m_Node": { @@ -672,9 +624,9 @@ }, "m_InputSlot": { "m_Node": { - "m_Id": "4861c23078fe4af0a8fde3e41d7c6f90" + "m_Id": "293d0108d9584aa484fde050460d9f45" }, - "m_SlotId": 2 + "m_SlotId": 0 } }, { @@ -1194,30 +1146,6 @@ "m_Labels": [] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "137feff6cdc24354b69217b248eb264b", - "m_Id": 1, - "m_DisplayName": "True", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "True", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 1.0, - "z": 0.0, - "w": 1.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -1802,30 +1730,6 @@ ] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "2c9edab1d38c47ab996ec26fa314cb8b", - "m_Id": 2, - "m_DisplayName": "False", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "False", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Texture2DInputMaterialSlot", @@ -2103,67 +2007,6 @@ ] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.BranchNode", - "m_ObjectId": "4861c23078fe4af0a8fde3e41d7c6f90", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Branch", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 779.5, - "y": 340.0, - "width": 170.0, - "height": 142.0 - } - }, - "m_Slots": [ - { - "m_Id": "cf460984f0b04abc9a1eb76b4653c58d" - }, - { - "m_Id": "137feff6cdc24354b69217b248eb264b" - }, - { - "m_Id": "2c9edab1d38c47ab996ec26fa314cb8b" - }, - { - "m_Id": "78558bf43c9444daa1ec09e2e4a30e81" - } - ], - "synonyms": [ - "switch", - "if", - "else" - ], - "m_Precision": 0, - "m_PreviewExpanded": false, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - } -} - -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", - "m_ObjectId": "4a6066bf990a4d159d9659b683783b90", - "m_Id": 0, - "m_DisplayName": "A", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "A", - "m_StageCapability": 3, - "m_Value": 0.0, - "m_DefaultValue": 0.0, - "m_Labels": [] -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -3130,74 +2973,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "78558bf43c9444daa1ec09e2e4a30e81", - "m_Id": 3, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.ComparisonNode", - "m_ObjectId": "7cb37beeca794a5b89fb91fdcb9568fc", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Comparison", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 605.0, - "y": 259.0, - "width": 145.0, - "height": 135.5 - } - }, - "m_Slots": [ - { - "m_Id": "4a6066bf990a4d159d9659b683783b90" - }, - { - "m_Id": "da61220af4404dc5bb72371b8ca2a026" - }, - { - "m_Id": "e5a3f628c5774b608259c77d07beee34" - } - ], - "synonyms": [ - "equal", - "greater than", - "less than" - ], - "m_Precision": 0, - "m_PreviewExpanded": true, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - }, - "m_ComparisonType": 0 -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", @@ -4288,8 +4063,8 @@ "m_ObjectId": "c2b337bafb404d6c929509f7ce06025b", "m_Title": "default sampling causes errors", "m_Position": { - "x": -587.0, - "y": -253.99996948242188 + "x": -586.9999389648438, + "y": -253.5 } } @@ -4442,20 +4217,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.BooleanMaterialSlot", - "m_ObjectId": "cf460984f0b04abc9a1eb76b4653c58d", - "m_Id": 0, - "m_DisplayName": "Predicate", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "Predicate", - "m_StageCapability": 3, - "m_Value": false, - "m_DefaultValue": false -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.PropertyNode", @@ -4608,8 +4369,8 @@ "m_ObjectId": "d60fae789e9b4436b8bea1089238c3a4", "m_Title": "Color", "m_Position": { - "x": -179.76116943359376, - "y": 805.5347900390625 + "x": -179.9998779296875, + "y": 805.5000610351563 } } @@ -4762,21 +4523,6 @@ ] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", - "m_ObjectId": "da61220af4404dc5bb72371b8ca2a026", - "m_Id": 1, - "m_DisplayName": "B", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "B", - "m_StageCapability": 3, - "m_Value": 0.0, - "m_DefaultValue": 0.0, - "m_Labels": [] -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", @@ -4893,20 +4639,6 @@ "m_Labels": [] } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.BooleanMaterialSlot", - "m_ObjectId": "e5a3f628c5774b608259c77d07beee34", - "m_Id": 2, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": false, - "m_DefaultValue": false -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", diff --git a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph index 7a6bb26ff..27383eb36 100644 --- a/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph +++ b/Runtime/Mapbox/BaseModule/Unity/Visuals/Shaders/MapboxTerrainBilinearSampling.shadersubgraph @@ -237,12 +237,6 @@ { "m_Id": "95601c21895b45c79d7bf1b0e1f87915" }, - { - "m_Id": "5bfc6c34651340ba82aac1304e277bec" - }, - { - "m_Id": "1e3bd0f2cb1444ed84a95d0d8529b787" - }, { "m_Id": "f401cfa820c649fab90cf2060324b8f4" }, @@ -599,20 +593,6 @@ "m_SlotId": 1 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "1e3bd0f2cb1444ed84a95d0d8529b787" - }, - "m_SlotId": 1 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "f401cfa820c649fab90cf2060324b8f4" - }, - "m_SlotId": 0 - } - }, { "m_OutputSlot": { "m_Node": { @@ -1201,20 +1181,6 @@ "m_SlotId": 2 } }, - { - "m_OutputSlot": { - "m_Node": { - "m_Id": "5bfc6c34651340ba82aac1304e277bec" - }, - "m_SlotId": 1 - }, - "m_InputSlot": { - "m_Node": { - "m_Id": "f401cfa820c649fab90cf2060324b8f4" - }, - "m_SlotId": 1 - } - }, { "m_OutputSlot": { "m_Node": { @@ -1602,9 +1568,9 @@ }, "m_InputSlot": { "m_Node": { - "m_Id": "5bfc6c34651340ba82aac1304e277bec" + "m_Id": "f401cfa820c649fab90cf2060324b8f4" }, - "m_SlotId": 0 + "m_SlotId": 1 } }, { @@ -1658,7 +1624,7 @@ }, "m_InputSlot": { "m_Node": { - "m_Id": "1e3bd0f2cb1444ed84a95d0d8529b787" + "m_Id": "f401cfa820c649fab90cf2060324b8f4" }, "m_SlotId": 0 } @@ -3439,7 +3405,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -3610,7 +3576,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -4027,10 +3993,10 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": 960.0, - "y": -1482.0001220703125, + "x": 830.9999389648438, + "y": -2427.499755859375, "width": 101.0, - "height": 125.0001220703125 + "height": 101.0 } }, "m_Slots": [ @@ -4373,42 +4339,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.NormalizeNode", - "m_ObjectId": "1e3bd0f2cb1444ed84a95d0d8529b787", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Normalize", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 436.9999084472656, - "y": -2220.500244140625, - "width": 131.50003051757813, - "height": 94.0 - } - }, - "m_Slots": [ - { - "m_Id": "5c9da9cc6e0a4488a187f0607f577096" - }, - { - "m_Id": "982f399221354351b5739e6e63fafe07" - } - ], - "synonyms": [], - "m_Precision": 0, - "m_PreviewExpanded": false, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -4436,9 +4366,9 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": 765.5001220703125, - "y": -2302.000244140625, - "width": 131.5, + "x": 628.5, + "y": -2301.999755859375, + "width": 131.49993896484376, "height": 94.0 } }, @@ -5821,7 +5751,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -5858,30 +5788,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "3cf82528fe81462f93066232bed3b068", - "m_Id": 0, - "m_DisplayName": "In", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "In", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.UVMaterialSlot", @@ -7236,66 +7142,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.NormalizeNode", - "m_ObjectId": "5bfc6c34651340ba82aac1304e277bec", - "m_Group": { - "m_Id": "" - }, - "m_Name": "Normalize", - "m_DrawState": { - "m_Expanded": true, - "m_Position": { - "serializedVersion": "2", - "x": 436.9999084472656, - "y": -2366.000244140625, - "width": 131.50003051757813, - "height": 94.0 - } - }, - "m_Slots": [ - { - "m_Id": "3cf82528fe81462f93066232bed3b068" - }, - { - "m_Id": "c011d63ed6f54f1e98ef4155088061ff" - } - ], - "synonyms": [], - "m_Precision": 0, - "m_PreviewExpanded": false, - "m_DismissedVersion": 0, - "m_PreviewMode": 0, - "m_CustomColors": { - "m_SerializableColors": [] - } -} - -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "5c9da9cc6e0a4488a187f0607f577096", - "m_Id": 0, - "m_DisplayName": "In", - "m_SlotType": 0, - "m_Hidden": false, - "m_ShaderOutputName": "In", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", @@ -9449,8 +9295,8 @@ "m_ObjectId": "8f619ecde74e498bbff41c586ba67e16", "m_Title": "ambient occlusion like effect", "m_Position": { - "x": -1972.0001220703125, - "y": -68.0 + "x": -2524.0, + "y": -69.0 } } @@ -10012,30 +9858,6 @@ "m_BareResource": false } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "982f399221354351b5739e6e63fafe07", - "m_Id": 1, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", @@ -11523,7 +11345,7 @@ "m_ShaderOutputName": "A", "m_StageCapability": 3, "m_Value": { - "x": 1.0, + "x": 2.0, "y": 0.0, "z": 0.0, "w": 0.0 @@ -12065,30 +11887,6 @@ } } -{ - "m_SGVersion": 0, - "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", - "m_ObjectId": "c011d63ed6f54f1e98ef4155088061ff", - "m_Id": 1, - "m_DisplayName": "Out", - "m_SlotType": 1, - "m_Hidden": false, - "m_ShaderOutputName": "Out", - "m_StageCapability": 3, - "m_Value": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - }, - "m_DefaultValue": { - "x": 0.0, - "y": 0.0, - "z": 0.0, - "w": 0.0 - } -} - { "m_SGVersion": 0, "m_Type": "UnityEditor.ShaderGraph.DivideNode", @@ -14999,8 +14797,8 @@ "m_ObjectId": "f3bbc84efc26440dbb48712a59f0bacf", "m_Title": "older elevation and color section", "m_Position": { - "x": -2350.5, - "y": -1693.5 + "x": -2350.0, + "y": -1694.0 } } @@ -15016,9 +14814,9 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": 611.0, - "y": -2306.000244140625, - "width": 129.49993896484376, + "x": 473.9999694824219, + "y": -2305.999755859375, + "width": 129.50003051757813, "height": 118.0 } }, diff --git a/Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs b/Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs similarity index 100% rename from Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs rename to Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs diff --git a/Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs.meta b/Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Utility/Attributes/GameObjectLayerAttribute.cs.meta rename to Runtime/Mapbox/BaseModule/Utilities/Attributes/GameObjectLayerAttribute.cs.meta diff --git a/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs new file mode 100644 index 000000000..384575b8c --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace Mapbox.BaseModule.Utilities.Attributes +{ + /// + /// Attribute to specify the URL string representation of an enum value. + /// Used for converting enum values to their API URL parameter equivalents. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class MapboxRequestStringAttribute : Attribute + { + private readonly string urlString; + + /// Gets the URL string value. + public string Value { get { return urlString; } } + + /// + /// Initializes a new instance of the MapboxRequestStringAttribute. + /// + /// The URL string representation of the enum value. + public MapboxRequestStringAttribute(string urlString) + { + this.urlString = urlString; + } + } +} diff --git a/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta new file mode 100644 index 000000000..9f407219b --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/Attributes/MapboxRequestStringAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcd69265fabd54b68849c8404f514397 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs new file mode 100644 index 000000000..f632c6928 --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Reflection; +using Mapbox.BaseModule.Utilities.Attributes; + +namespace Mapbox.BaseModule.Utilities +{ + /// + /// Extension methods for Enum types. + /// + public static class EnumExtensions + { + /// + /// Gets the URL string representation of an enum value. + /// Looks for the UrlStringAttribute on the enum value. + /// If no attribute is found, returns the enum's ToString() value. + /// + /// The enum value. + /// The URL string from the UrlStringAttribute, or ToString() if not found. + public static string MapboxTypeDescription(this Enum value) + { + if (value == null) + { + return string.Empty; + } + + Type type = value.GetType(); + string name = Enum.GetName(type, value); + + if (string.IsNullOrEmpty(name)) + { + return value.ToString(); + } + + FieldInfo field = type.GetField(name); + + if (field == null) + { + return value.ToString(); + } + + // Try to get the UrlStringAttribute + MapboxRequestStringAttribute attribute = Attribute.GetCustomAttribute(field, typeof(MapboxRequestStringAttribute)) as MapboxRequestStringAttribute; + + if (attribute != null) + { + return attribute.Value; + } + + // Fallback to ToString if no attribute found + return value.ToString(); + } + } +} diff --git a/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta new file mode 100644 index 000000000..802ac2edd --- /dev/null +++ b/Runtime/Mapbox/BaseModule/Utilities/EnumExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 71ba70e686ffd4723872a8d98a7af3e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs b/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs index c0a255fd4..f81b4d31d 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/FlatTerrainStrategy.cs @@ -1,13 +1,19 @@ -using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Unity; using Mapbox.ImageModule.Terrain.Settings; using Mapbox.ImageModule.Terrain.TerrainStrategies; using UnityEngine; namespace Mapbox.BaseModule.Utilities { + /// + /// Terrain strategy used when no terrain module is present. Writes a shared 4-vertex + /// flat quad into each tile's mesh and (optionally) shares that quad as a MeshCollider. + /// Has no elevation sampling; ignores SimplificationFactor. + /// public class FlatTerrainStrategy : TerrainStrategy { MeshDataArray _cachedQuad; + private TerrainColliderOptions _colliderOptions; public int RequiredVertexCount => 4; @@ -15,13 +21,31 @@ public FlatTerrainStrategy() { BuildQuad(); } - + + /// + /// Captures the subset of this strategy cares + /// about (currently just collider options). Safe to pass null. + /// + public override void Initialize(ElevationLayerProperties elOptions) + { + if (elOptions != null) + { + _colliderOptions = elOptions.colliderOptions; + } + } + + /// + /// Writes the flat quad into 's mesh if not already present, + /// and optionally attaches a MeshCollider backed by the same quad. The + /// flag is unused — flat terrain has no + /// elevation. + /// public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) { var meshFilter = tile.MeshFilter; if (meshFilter.sharedMesh.vertexCount != RequiredVertexCount) { - + var sharedMesh = tile.MeshFilter.sharedMesh; sharedMesh.Clear(); @@ -36,12 +60,24 @@ public override void RegisterTile(UnityMapTile tile, bool createElevatedMesh) } tile.MeshVertexCount = RequiredVertexCount; + + if (_colliderOptions != null && _colliderOptions.addCollider) + { + var meshCollider = tile.GetComponent(); + if (meshCollider == null) + { + meshCollider = tile.gameObject.AddComponent(); + } + // The flat quad is already the collision surface; reuse it directly rather + // than allocating a dedicated mesh as ElevatedTerrainStrategy does. + meshCollider.sharedMesh = meshFilter.sharedMesh; + } } - + private void BuildQuad() { var size = 1; - + //32 //01 var verts = new Vector3[4]; diff --git a/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs b/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs index e32977698..d310ffa66 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/MapBehaviourCore.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using Mapbox.BaseModule.Map; using UnityEngine; @@ -15,9 +16,9 @@ public abstract class MapBehaviourCore : MonoBehaviour : InitializationStatus.WaitingForInitialization; public Action Initialized = (m) => { }; - public virtual void Initialize() + public virtual IEnumerator Initialize() { - + yield return null; } } } diff --git a/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs b/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs index 34bada0e2..2e61d5c4a 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/MapShifterCore.cs @@ -28,12 +28,17 @@ public void Update() if(float.IsNaN(viewCenterPosition.x) || float.IsNaN(viewCenterPosition.y) || float.IsInfinity(viewCenterPosition.x) || float.IsInfinity(viewCenterPosition.y)) return; - if (Mathf.Abs(viewCenterPosition.x) > _shiftRange.x || + if (Mathf.Abs(viewCenterPosition.x) > _shiftRange.x || Mathf.Abs(viewCenterPosition.z) > _shiftRange.y) { var centerLatLng = _mapInformation.ConvertPositionToLatLng(viewCenterPosition); _mapInformation.SetLatitudeLongitude(centerLatLng); - _unityContext.RuntimeGenerationRoot.position -= viewCenterPosition; + // localPosition (not position) so the shift stays in MapRoot-local space + // and composes with any parent transform. Writing world-space here would + // anchor RuntimeGenerationRoot in world coordinates — Unity would + // back-compute a localPosition that counters the parent rotation, and + // vector content would visually stop rotating with the rest of the map. + _unityContext.RuntimeGenerationRoot.localPosition -= viewCenterPosition; } } diff --git a/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs b/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs index d66e6f582..e465676e5 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/PolylineUtils.cs @@ -25,6 +25,11 @@ public static class PolylineUtils /// List of making up the line. public static List Decode(string encodedPath, int precision = 5) { + if (string.IsNullOrEmpty(encodedPath)) + { + return new List(); + } + int len = encodedPath.Length; double factor = Math.Pow(10, precision); diff --git a/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs b/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs index b3e8194de..047ecfea1 100644 --- a/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs +++ b/Runtime/Mapbox/BaseModule/Utilities/VectorExtensions.cs @@ -141,5 +141,26 @@ public static LatitudeLongitude GetGeoPosition(this Vector2 position, Vector2d r { return position.ToVector3xz().GetGeoPosition(refPoint, scale); } + + /// + /// Raycast from a screen position onto a plane and return the intersection point. + /// Safer than manual ray math — handles edge cases like rays parallel to the plane. + /// + /// Camera to raycast from. + /// Target plane for intersection. + /// Screen-space position (e.g. Input.mousePosition). + /// World-space intersection point. Zero if no intersection. + /// True if the ray intersects the plane. + public static bool GetPlaneIntersection(this Camera camera, Plane plane, Vector3 screenPosition, out Vector3 hit) + { + hit = Vector3.zero; + var ray = camera.ScreenPointToRay(screenPosition); + if (plane.Raycast(ray, out var distance)) + { + hit = ray.GetPoint(distance); + return true; + } + return false; + } } } diff --git a/Runtime/Mapbox/CustomImageryModule.meta b/Runtime/Mapbox/CustomImageryModule.meta new file mode 100644 index 000000000..ee0e1a460 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9957d55bdc494407c87f742bb99a0cc8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs new file mode 100644 index 000000000..007c55f86 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs @@ -0,0 +1,55 @@ +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.ImageModule; +using Mapbox.UnityMapService; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + public class CustomApiLayerModuleScript : ModuleConstructorScript + { + public CustomSourceSettings CustomSourceSettings; + public StaticLayerModuleSettings Settings = new StaticLayerModuleSettings() + { + RejectTilesOutsideZoom = new Vector2(2, 25), + DataSettings = new ImageSourceSettings() + { + ClampDataLevelToMax = 25 + } + }; + public override ILayerModule ModuleImplementation { get; protected set; } + + private void Start() + { + + } + + public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, + UnityContext unityContext) + { + if (Settings.SourceType == ImagerySourceType.None) + { + + } + else if (Settings.SourceType == ImagerySourceType.Custom) + { + Settings.DataSettings.TilesetId = Settings.CustomSourceId; + } + else + { + var imageryTileset = MapboxDefaultImagery.GetParameters(Settings.SourceType); + Settings.DataSettings.TilesetId = imageryTileset.Id; + } + + var unityService = service as MapUnityService; + var source = new CustomSource(CustomSourceSettings, unityService.FetchingManager, unityService.CacheManager, new ImageSourceSettings() + { + TilesetId = "CustomImagery" + }); + ModuleImplementation = new StaticApiLayerModule(source, Settings); + return ModuleImplementation; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta new file mode 100644 index 000000000..64db1dc66 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomApiLayerModuleScript.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6aed0ab09c9749069b3359d261d35ae4 +timeCreated: 1767099820 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs new file mode 100644 index 000000000..80ccabcf8 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs @@ -0,0 +1,58 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.UnityMapService.DataSources; + +namespace Mapbox.CustomImageryModule +{ + public class CustomSource : ImageSource + { + protected CustomSourceSettings _customSourceSettings; + protected ImageSourceSettings _settings; + + public CustomSource(CustomSourceSettings customSourceSettings, DataFetchingManager dataFetchingManager, + MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) + : base(dataFetchingManager, mapboxCacheManager, settings) + { + _settings = settings; + _customSourceSettings = customSourceSettings; + } + + protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) + { + // Empty UrlFormat: fall back to the base raster path (URL constructed from + // tilesetId by the source). Without this fallback CustomTMSTile.Initialize + // would call string.Format(null, …) and throw. + if (string.IsNullOrEmpty(_customSourceSettings.UrlFormat)) + { + return new RasterTile(tileId, tilesetId, true); + } + return new CustomTMSTile(_customSourceSettings.UrlFormat, tileId, tilesetId, true, _customSourceSettings.InvertY, _customSourceSettings.IsMapboxService); + } + + protected override RasterData CreateRasterDataWrapper(RasterTile tile) + { + RasterData rasterData; + rasterData = new RasterData() + { + TileId = tile.Id, + TilesetId = tile.TilesetId, + Texture = tile.Texture2D, + CacheType = tile.FromCache, + Data = tile.Data, + ETag = tile.ETag, + ExpirationDate = tile.ExpirationDate + }; + + return rasterData; + } + + protected override void CheckExpiration(RasterData cacheItem) + { + //not checking for expiration and updating for custom sources + //just using whatever is in cache + return; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta new file mode 100644 index 000000000..5e19cd35a --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSource.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: db00866e0fd14890ad560a64229aae45 +timeCreated: 1767707026 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs new file mode 100644 index 000000000..b28398fdd --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs @@ -0,0 +1,16 @@ +using System; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + [Serializable] + public class CustomSourceSettings + { + [Tooltip("Url format string, structured as C# string format with '{}' fields for X/Y/Z coordinates. {0}=Z, {1}=X, {2}=Y")] + public string UrlFormat; + [Tooltip("Invert Y axis coordinates for TMS coordinate system, which starts from bottom left and grows to top-right")] + public bool InvertY; + [Tooltip("Enable when the URL points to a Mapbox-hosted tileset (api.mapbox.com). Appends access_token and sku query parameters automatically.")] + public bool IsMapboxService; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta new file mode 100644 index 000000000..cd1886295 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomSourceSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 83855a3135724ccd938059a88012027d +timeCreated: 1767707253 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs new file mode 100644 index 000000000..7ebd0233a --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs @@ -0,0 +1,48 @@ +using System; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Tiles; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + /// + /// TMS tile in the name means Y axis is inverted in the tile ids + /// + public class CustomTMSTile : RasterTile + { + private string _urlFormat; + private bool _invertY; + private bool _isMapboxService; + + // Legacy 4-arg constructor preserved so external subclasses compiled against the + // 3.0.6 API keep building. Defaults match the pre-3.0.7 behavior (invertY = true, + // non-Mapbox-service URL request path). + public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture) + : this(urlFormat, tileId, tilesetId, useNonReadableTexture, invertY: true, isMapboxService: false) { } + + public CustomTMSTile(string urlFormat, CanonicalTileId tileId, string tilesetId, bool useNonReadableTexture, bool invertY, bool isMapboxService) : base(tileId, tilesetId, useNonReadableTexture) + { + _urlFormat = urlFormat; + _invertY = invertY; + _isMapboxService = isMapboxService; + } + + public override void Initialize(IFileSource fileSource, Action p) + { + TileState = TileState.Loading; + _callback = p; + + var y = _invertY ? (int)(Mathf.Pow(2, Id.Z) - Id.Y - 1) : Id.Y; + _generatedUrl = string.Format(_urlFormat, Id.Z, Id.X, y); + DoTheRequest(fileSource); + } + + protected override void DoTheRequest(IFileSource fileSource) + { + _webRequest = _isMapboxService + ? fileSource.MapboxImageRequest(_generatedUrl, HandleTileResponse, ETag, 10, IsTextureNonreadable) + : fileSource.CustomImageRequest(_generatedUrl, HandleTileResponse, ETag, 10, IsTextureNonreadable); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta new file mode 100644 index 000000000..25a8c0e9f --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTMSTile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f1f46f6d333e4d25a4ccd278b71cecdc +timeCreated: 1767706943 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs new file mode 100644 index 000000000..75acc3a9c --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs @@ -0,0 +1,43 @@ +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.ImageModule.Terrain; +using Mapbox.UnityMapService; +using UnityEngine; + +namespace Mapbox.CustomImageryModule +{ + public class CustomTerrainLayerModuleScript : ModuleConstructorScript + { + public CustomSourceSettings CustomSourceSettings; + public TerrainLayerModuleSettings Settings = new TerrainLayerModuleSettings() + { + RejectTilesOutsideZoom = new Vector2(2, 25), + DataSettings = new ImageSourceSettings() + { + ClampDataLevelToMax = 25 + } + }; + public override ILayerModule ModuleImplementation { get; protected set; } + + private void Start() + { + + } + + public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, + UnityContext unityContext) + { + Settings.DataSettings.TilesetId = "CustomTerrain"; + + // Pass Settings.DataSettings through so cache size, retina flag, non-readable + // textures, and data-zoom clamp from the inspector reach CustomTerrainSource. + // Previously a throwaway ImageSourceSettings was used here and the user's + // inspector values never took effect. + var unityService = service as MapUnityService; + var source = new CustomTerrainSource(CustomSourceSettings, unityService.FetchingManager, unityService.CacheManager, Settings.DataSettings); + ModuleImplementation = new TerrainLayerModule(source, Settings); + return ModuleImplementation; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta new file mode 100644 index 000000000..ccdbd7c1a --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainLayerModuleScript.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 83ddcee6d2844b7e9f15508beb22c72b +timeCreated: 1767632785 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs new file mode 100644 index 000000000..85d9b4315 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs @@ -0,0 +1,54 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.UnityMapService.DataSources; + +namespace Mapbox.CustomImageryModule +{ + public class CustomTerrainSource : TerrainSource + { + protected CustomSourceSettings _customSourceSettings; + protected ImageSourceSettings _settings; + + public CustomTerrainSource(CustomSourceSettings customSettings, DataFetchingManager dataFetchingManager, MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) + : base(dataFetchingManager, mapboxCacheManager, settings) + { + _customSourceSettings = customSettings; + _settings = settings; + } + + protected override RasterTile CreateTile(CanonicalTileId tileId, string tilesetId) + { + // Empty UrlFormat: fall back to plain RasterTile so the base path's URL + // construction is used. Without this, CustomTMSTile.Initialize would call + // string.Format(null, …) and throw. Mirrors the same fallback in CustomSource. + if (string.IsNullOrEmpty(_customSourceSettings.UrlFormat)) + { + return new RasterTile(tileId, tilesetId, true); + } + return new CustomTMSTile( + _customSourceSettings.UrlFormat, + tileId, tilesetId, true, + _customSourceSettings.InvertY, + _customSourceSettings.IsMapboxService); + } + + protected override TerrainData CreateRasterDataWrapper(RasterTile tile) + { + TerrainData rasterData; + rasterData = new TerrainData() + { + TileId = tile.Id, + TilesetId = tile.TilesetId, + Texture = tile.Texture2D, + CacheType = tile.FromCache, + Data = tile.Data, + ETag = tile.ETag, + ExpirationDate = tile.ExpirationDate + }; + + return rasterData; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta new file mode 100644 index 000000000..29c8d3b8c --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/CustomTerrainSource.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfc3281b14f542aa8d2164cabb8967c7 +timeCreated: 1767707245 \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/Editor.meta b/Runtime/Mapbox/CustomImageryModule/Editor.meta new file mode 100644 index 000000000..68a67e75e --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0b6b51283dd7d417a962d32657e3e2f5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs new file mode 100644 index 000000000..5b05645c1 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs @@ -0,0 +1,46 @@ +using UnityEditor; + +namespace Mapbox.CustomImageryModule.Editor +{ + [CustomEditor(typeof(CustomApiLayerModuleScript))] + public class CustomApiLayerModuleScriptEditor : UnityEditor.Editor + { + SerializedProperty customSourceSettingsProp; + + SerializedProperty rejectTilesOutsideZoomProp; + SerializedProperty clampDataLevelToMaxProp; + + void OnEnable() + { + // Root properties + customSourceSettingsProp = serializedObject.FindProperty("CustomSourceSettings"); + + // Settings.RejectTilesOutsideZoom + rejectTilesOutsideZoomProp = + serializedObject.FindProperty("Settings") + .FindPropertyRelative("RejectTilesOutsideZoom"); + + // Settings.DataSettings.ClampDataLevelToMax + clampDataLevelToMaxProp = + serializedObject.FindProperty("Settings") + .FindPropertyRelative("DataSettings") + .FindPropertyRelative("ClampDataLevelToMax"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField("Custom Source Settings", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(customSourceSettingsProp, true); + + EditorGUILayout.Space(); + + EditorGUILayout.LabelField("Tile Settings", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(rejectTilesOutsideZoomProp); + EditorGUILayout.PropertyField(clampDataLevelToMaxProp); + + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta new file mode 100644 index 000000000..e758ce0e4 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomApiLayerModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c31619c32fa2549edbbc6761da29703b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs b/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs new file mode 100644 index 000000000..de978998c --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs @@ -0,0 +1,34 @@ +using Mapbox.ImageModule.Editor; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.CustomImageryModule.Editor +{ + /// + /// Custom inspector for CustomTerrainLayerModuleScript. Shares rendering logic + /// with the main terrain editor via . + /// + [CustomEditor(typeof(CustomTerrainLayerModuleScript))] + public class CustomTerrainLayerModuleScriptEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script")); + } + + DrawPropertiesExcluding(serializedObject, "m_Script", "Settings"); + + var settingsProp = serializedObject.FindProperty("Settings"); + if (settingsProp != null) + { + TerrainSettingsInspectorHelper.Draw(settingsProp); + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta b/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta new file mode 100644 index 000000000..9fa7ef55b --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/CustomTerrainLayerModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c8ba464cece144a6a06c5456579af2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef new file mode 100644 index 000000000..6fff7ca1b --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "MapboxCustomImageryModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:60dcad938ceef450e9b8d773e5c61b99", + "GUID:15fe16679907f4627ab38b784df8ca99" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta new file mode 100644 index 000000000..76a5b8ec5 --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/Editor/MapboxCustomImageryModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4c80974ccb411472e8e926051beeec6f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef new file mode 100644 index 000000000..34eca74fd --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxCustomImageryModule", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:36ca6af6e2b304d4090888554d6ce199", + "GUID:5ff00896142b94bc7a58a7c72b2faf57" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta new file mode 100644 index 000000000..3e2d8598f --- /dev/null +++ b/Runtime/Mapbox/CustomImageryModule/MapboxCustomImageryModule.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 60dcad938ceef450e9b8d773e5c61b99 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/Example.meta b/Runtime/Mapbox/DirectionsApi/Example.meta new file mode 100644 index 000000000..5af60e229 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ba43b7487748483f9602f4aec0e1766 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs new file mode 100644 index 000000000..a21905a05 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs @@ -0,0 +1,113 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.DirectionsApi.Response; +using UnityEngine; + +namespace Mapbox.DirectionsApi.Example +{ + /// + /// Example script demonstrating basic usage of the Mapbox Directions API. + /// Calculates a route between two coordinates and logs the result. + /// + public class DirectionsExample : MonoBehaviour + { + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Route Settings")] + public RoutingProfile.RoutingProfileOptions Profile = RoutingProfile.RoutingProfileOptions.Driving; + + [Header("Test Coordinates (Default: SF to LA)")] + public double OriginLatitude = 37.7749; + public double OriginLongitude = -122.4194; + public double DestinationLatitude = 34.0522; + public double DestinationLongitude = -118.2437; + + private MapboxDirectionsApi _directionsApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _directionsApi = new MapboxDirectionsApi(map.MapService.FileSource); + Debug.Log("Directions API initialized. Getting route..."); + GetRoute(); + } + + public void GetRoute() + { + if (_directionsApi == null) + { + Debug.LogWarning("Directions API not initialized yet. Waiting for map..."); + return; + } + + // Create coordinates + var origin = new LatitudeLongitude(OriginLatitude, OriginLongitude); + var destination = new LatitudeLongitude(DestinationLatitude, DestinationLongitude); + + // Create a directions request + var directionResource = new DirectionResource( + new[] { origin, destination }, + RoutingProfile.GetProfile(Profile) + ) + { + Alternatives = false, + Steps = false + }; + + // Query the API + _directionsApi.Query(directionResource, HandleDirectionsResponse); + } + + void HandleDirectionsResponse(DirectionsResponse response) + { + if (response == null) + { + Debug.LogError("Directions API returned null response"); + return; + } + + if (response.Routes == null || response.Routes.Count == 0) + { + Debug.LogWarning($"No route found. Response code: {response.Code}"); + return; + } + + // Log the first route + var route = response.Routes[0]; + Debug.Log("=== Route Found ==="); + Debug.Log($"Profile: {Profile}"); + Debug.Log($"Distance: {route.Distance:F2} meters ({route.Distance / 1000:F2} km)"); + Debug.Log($"Duration: {route.Duration:F0} seconds ({route.Duration / 60:F1} minutes)"); + Debug.Log($"Weight: {route.Weight}"); + Debug.Log($"Geometry points: {route.Geometry.Count}"); + + // Log first and last geometry points + if (route.Geometry != null && route.Geometry.Count > 0) + { + var firstPoint = route.Geometry[0]; + var lastPoint = route.Geometry[route.Geometry.Count - 1]; + Debug.Log($"Start point: ({firstPoint.x}, {firstPoint.y})"); + Debug.Log($"End point: ({lastPoint.x}, {lastPoint.y})"); + } + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + } +} diff --git a/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta new file mode 100644 index 000000000..8de4d888a --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/DirectionsExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25d7947494b1f46b28eeb2a8124c2e4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs new file mode 100644 index 000000000..96a5911d7 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs @@ -0,0 +1,234 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.DirectionsApi.MapMatching; +using UnityEngine; + +namespace Mapbox.DirectionsApi.Example +{ + /// + /// Example script demonstrating the Mapbox Map Matching API. + /// Map matching snaps GPS traces to the road network, cleaning up noisy GPS data. + /// + public class MapMatchingExample : MonoBehaviour + { + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Map Matching Settings")] + [Tooltip("Profile to use for matching (driving, walking, cycling, etc.)")] + public Profile MatchingProfile = Profile.MapboxDriving; + + [Tooltip("Request timeout in seconds")] + public int TimeoutSeconds = 30; + + [Header("GPS Trace Configuration")] + [Tooltip("Generate a test GPS trace automatically on start")] + public bool UseTestTrace = true; + + [Header("Test Trace (San Francisco area)")] + [Tooltip("Simulated noisy GPS coordinates")] + public Vector2d[] TestGPSTrace = new Vector2d[] + { + new Vector2d(37.7749, -122.4194), + new Vector2d(37.7751, -122.4193), + new Vector2d(37.7753, -122.4191), + new Vector2d(37.7755, -122.4189), + new Vector2d(37.7757, -122.4187), + new Vector2d(37.7759, -122.4185), + new Vector2d(37.7761, -122.4183), + new Vector2d(37.7763, -122.4181) + }; + + [Header("Optional Parameters")] + [Tooltip("GPS accuracy radius for each point (0 = use default). Higher for noisy traces.")] + public uint DefaultRadius = 10; + + [Tooltip("Enable to remove clusters and resample traces")] + public bool TidyTrace = false; + + [Tooltip("Request turn-by-turn steps")] + public bool IncludeSteps = false; + + [Tooltip("Overview geometry detail")] + public MapMatching.Overview GeometryOverview = MapMatching.Overview.Simplified; + + private MapboxMapMatcherApi _mapMatcherApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _mapMatcherApi = new MapboxMapMatcherApi(map.MapService.FileSource, TimeoutSeconds); + Debug.Log("Map Matching API initialized."); + + if (UseTestTrace) + { + Debug.Log("Matching test GPS trace..."); + MatchGPSTrace(TestGPSTrace); + } + } + + /// + /// Match a GPS trace to the road network. + /// + /// Array of GPS coordinates to match + public void MatchGPSTrace(Vector2d[] gpsTrace) + { + if (_mapMatcherApi == null) + { + Debug.LogWarning("Map Matching API not initialized yet. Waiting for map..."); + return; + } + + if (gpsTrace == null || gpsTrace.Length < 2) + { + Debug.LogError("GPS trace must have at least 2 coordinates"); + return; + } + + if (gpsTrace.Length > 100) + { + Debug.LogError("GPS trace cannot have more than 100 coordinates"); + return; + } + + // Create map matching request + var mapMatchingResource = new MapMatchingResource + { + Coordinates = gpsTrace, + Profile = MatchingProfile, + Steps = IncludeSteps, + Overview = GeometryOverview, + Tidy = TidyTrace + }; + + // Set radiuses if specified + if (DefaultRadius > 0) + { + uint[] radiuses = new uint[gpsTrace.Length]; + for (int i = 0; i < radiuses.Length; i++) + { + radiuses[i] = DefaultRadius; + } + mapMatchingResource.Radiuses = radiuses; + } + + Debug.Log($"Sending map matching request with {gpsTrace.Length} coordinates..."); + + // Execute the map matching request + _mapMatcherApi.Match(mapMatchingResource, HandleMapMatchingResponse); + } + + void HandleMapMatchingResponse(MapMatchingResponse response) + { + // Check for request errors + if (response.HasRequestError) + { + Debug.LogError("Map matching request failed:"); + foreach (var ex in response.RequestExceptions) + { + Debug.LogError($" {ex.Message}"); + } + return; + } + + // Check for matching errors + if (response.HasMatchingError) + { + Debug.LogError($"Map matching error: {response.MatchingError}"); + Debug.LogError($"Error code: {response.Code}"); + if (!string.IsNullOrEmpty(response.Message)) + { + Debug.LogError($"Message: {response.Message}"); + } + return; + } + + // Check if we got any matches + if (response.Matchings == null || response.Matchings.Length == 0) + { + Debug.LogWarning("No matches found for GPS trace"); + return; + } + + Debug.Log($"=== Map Matching Results ==="); + Debug.Log($"Found {response.Matchings.Length} matching(s)"); + + // Display each matching + for (int i = 0; i < response.Matchings.Length; i++) + { + var match = response.Matchings[i]; + Debug.Log($"\n--- Matching {i + 1} ---"); + Debug.Log($"Confidence: {match.Confidence:F2} (0=low, 1=high)"); + Debug.Log($"Distance: {match.Distance:F2} meters ({match.Distance / 1000:F2} km)"); + Debug.Log($"Duration: {match.Duration:F0} seconds ({match.Duration / 60:F1} minutes)"); + Debug.Log($"Weight: {match.Weight}"); + + if (match.Geometry != null) + { + Debug.Log($"Matched geometry points: {match.Geometry.Count}"); + Debug.Log($"Start: ({match.Geometry[0].x:F6}, {match.Geometry[0].y:F6})"); + Debug.Log($"End: ({match.Geometry[match.Geometry.Count - 1].x:F6}, {match.Geometry[match.Geometry.Count - 1].y:F6})"); + } + + if (match.Legs != null && match.Legs.Count > 0) + { + Debug.Log($"Route legs: {match.Legs.Count}"); + } + } + + // Display tracepoint information + if (response.Tracepoints != null && response.Tracepoints.Length > 0) + { + Debug.Log($"\n=== Tracepoints ({response.Tracepoints.Length}) ==="); + int unambiguousMatches = 0; + + for (int i = 0; i < response.Tracepoints.Length; i++) + { + var tracepoint = response.Tracepoints[i]; + if (tracepoint == null) + { + Debug.Log($"Tracepoint {i}: Omitted (removed by Tidy or unmatched)"); + continue; + } + + if (tracepoint.AlternativesCount == 0) + { + unambiguousMatches++; + } + + Debug.Log($"Tracepoint {i}:"); + Debug.Log($" Name: {tracepoint.Name}"); + Debug.Log($" Location: ({tracepoint.Location[1]}, {tracepoint.Location[0]})"); // lat, lng + Debug.Log($" Waypoint Index: {tracepoint.WaypointIndex}"); + Debug.Log($" Alternatives Count: {tracepoint.AlternativesCount} {(tracepoint.AlternativesCount == 0 ? "(unambiguous)" : "")}"); + } + + Debug.Log($"\nUnambiguous matches: {unambiguousMatches}/{response.Tracepoints.Length}"); + } + + // Summary + var bestMatch = response.Matchings[0]; + Debug.Log($"\n=== Summary ==="); + Debug.Log($"Best match confidence: {bestMatch.Confidence:F2}"); + Debug.Log($"GPS trace cleaned and snapped to {bestMatch.Geometry.Count} road points"); + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + } +} diff --git a/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta new file mode 100644 index 000000000..a015a2e39 --- /dev/null +++ b/Runtime/Mapbox/DirectionsApi/Example/MapMatchingExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ab2553fdd4a94bd0bee1cb811ff2802 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs index 97a313479..822bb7695 100644 --- a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs +++ b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingParameters.cs @@ -12,13 +12,13 @@ namespace Mapbox.DirectionsApi.MapMatching /// Directions profile id public enum Profile { - [Description("mapbox/driving")] + [MapboxRequestString("mapbox/driving")] MapboxDriving, - [Description("mapbox/driving-traffic")] + [MapboxRequestString("mapbox/driving-traffic")] MapboxDrivingTraffic, - [Description("mapbox/walking")] + [MapboxRequestString("mapbox/walking")] MapboxWalking, - [Description("mapbox/cycling")] + [MapboxRequestString("mapbox/cycling")] MapboxCycling } @@ -27,13 +27,13 @@ public enum Profile public enum Geometries { /// Default, precision 5. - [Description("polyline")] + [MapboxRequestString("polyline")] Polyline, /// Precision 6. - [Description("polyline6")] + [MapboxRequestString("polyline6")] Polyline6, /// Geojson. - [Description("geojson")] + [MapboxRequestString("geojson")] GeoJson } @@ -42,13 +42,13 @@ public enum Geometries public enum Overview { /// The most detailed geometry available - [Description("full")] + [MapboxRequestString("full")] Full, /// A simplified version of the full geometry - [Description("simplified")] + [MapboxRequestString("simplified")] Simplified, /// No overview geometry - [Description("false")] + [MapboxRequestString("false")] None } @@ -57,14 +57,14 @@ public enum Overview [System.Flags] public enum Annotations { - [Description("duration")] - Duration, - [Description("distance")] - Distance, - [Description("speed")] - Speed, - [Description("congestion")] - Congestion + [MapboxRequestString("duration")] + Duration = 1, + [MapboxRequestString("distance")] + Distance = 2, + [MapboxRequestString("speed")] + Speed = 4, + [MapboxRequestString("congestion")] + Congestion = 8 } @@ -73,41 +73,41 @@ public enum Annotations /// public enum InstructionLanguages { - [Description("de")] + [MapboxRequestString("de")] German, - [Description("en")] + [MapboxRequestString("en")] English, - [Description("eo")] + [MapboxRequestString("eo")] Esperanto, - [Description("es")] + [MapboxRequestString("es")] Spanish, - [Description("es-ES")] + [MapboxRequestString("es-ES")] SpanishSpain, - [Description("fr")] + [MapboxRequestString("fr")] French, - [Description("id")] + [MapboxRequestString("id")] Indonesian, - [Description("it")] + [MapboxRequestString("it")] Italian, - [Description("nl")] + [MapboxRequestString("nl")] Dutch, - [Description("pl")] + [MapboxRequestString("pl")] Polish, - [Description("pt-BR")] + [MapboxRequestString("pt-BR")] PortugueseBrazil, - [Description("ro")] + [MapboxRequestString("ro")] Romanian, - [Description("ru")] + [MapboxRequestString("ru")] Russian, - [Description("sv")] + [MapboxRequestString("sv")] Swedish, - [Description("tr")] + [MapboxRequestString("tr")] Turkish, - [Description("uk")] + [MapboxRequestString("uk")] Ukrainian, - [Description("vi")] + [MapboxRequestString("vi")] Vietnamese, - [Description("zh-Hans")] + [MapboxRequestString("zh-Hans")] ChineseSimplified } diff --git a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs index f8a397bd9..2c77185d8 100644 --- a/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs +++ b/Runtime/Mapbox/DirectionsApi/MapMatching/MapMatchingResource.cs @@ -9,6 +9,8 @@ using System.Linq; using Mapbox.BaseModule.Data.Platform; using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorTile.ExtensionMethods; namespace Mapbox.DirectionsApi.MapMatching { @@ -163,23 +165,22 @@ public override string GetUrl() Dictionary options = new Dictionary(); - return null; - // if (Geometries.HasValue) { options.Add("geometries", Geometries.Value.Description()); } - // if (null != _radiuses) { options.Add("radiuses", GetUrlQueryFromArray(_radiuses, ";")); } - // if (Steps.HasValue) { options.Add("steps", Steps.ToString().ToLower()); } - // if (Overview.HasValue) { options.Add("overview", Overview.Value.Description()); } - // if (null != _timestamps) { options.Add("timestamps", GetUrlQueryFromArray(_timestamps, ";")); } - // if (Annotations.HasValue) { options.Add("annotations", getUrlQueryFromAnnotations(Annotations.Value, ",")); } - // if (Tidy.HasValue) { options.Add("tidy", Tidy.Value.ToString().ToLower()); } - // if (Language.HasValue) { options.Add("language", Language.Value.Description()); } - // - // return - // Constants.Map.BaseAPI - // + _apiEndpoint - // + Profile.Description() + "/" - // + GetUrlQueryFromArray(_coordinates, ";") - // + ".json" - // + EncodeQueryString(options); + if (Geometries.HasValue) { options.Add("geometries", Geometries.Value.MapboxTypeDescription()); } + if (null != _radiuses) { options.Add("radiuses", GetUrlQueryFromArray(_radiuses, ";")); } + if (Steps.HasValue) { options.Add("steps", Steps.Value.ToString().ToLower()); } + if (Overview.HasValue) { options.Add("overview", Overview.Value.MapboxTypeDescription()); } + if (null != _timestamps) { options.Add("timestamps", GetUrlQueryFromArray(_timestamps, ";")); } + if (Annotations.HasValue) { options.Add("annotations", getUrlQueryFromAnnotations(Annotations.Value, ",")); } + if (Tidy.HasValue) { options.Add("tidy", Tidy.Value.ToString().ToLower()); } + if (Language.HasValue) { options.Add("language", Language.Value.MapboxTypeDescription()); } + + return + Constants.Map.BaseAPI + + _apiEndpoint + + Profile.MapboxTypeDescription() + "/" + + GetUrlQueryFromArray(_coordinates, ";") + + ".json" + + EncodeQueryString(options); } @@ -198,7 +199,7 @@ private string getUrlQueryFromAnnotations(Annotations anno, string separator) foreach (var a in Enum.GetValues(typeof(Annotations)).Cast()) { //if current value is set, add its description - //if (a == (anno & a)) { descriptions.Add(a.Description()); } + if (a == (anno & a)) { descriptions.Add(a.MapboxTypeDescription()); } } return string.Join(separator, descriptions.ToArray()); diff --git a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity index 5199689e4..ad89ad525 100644 --- a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity +++ b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/DirectionsDemo.unity @@ -910,7 +910,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 1316629400} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629398} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 2054936219} DataFetcher: {fileID: 2054936220} CacheManager: {fileID: 0} diff --git a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs index 357fdb748..0a9e8d25d 100644 --- a/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs +++ b/Runtime/Mapbox/DirectionsApi/Samples~/DirectionsApiDemo/Scripts/DirectionScript.cs @@ -10,7 +10,6 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; using Mapbox.DirectionsApi; -using Mapbox.Example.Scripts.ModuleBehaviours; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.UI; diff --git a/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs b/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs index 17f2b2c6a..47ea335c4 100644 --- a/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs +++ b/Runtime/Mapbox/Example/Editor/MapboxMapBehaviourEditor.cs @@ -11,9 +11,11 @@ public class MapboxMapBehaviourEditor : UnityEditor.Editor private SerializedProperty _unityContextProp; private SerializedProperty _tileCreatorProp; + private SerializedProperty _defaultTileMaterialProp; private SerializedProperty _tileProviderProp; private SerializedProperty _dataFetcherProp; private SerializedProperty _cacheManagerProp; + private SerializedProperty _locationFactoryProp; private SerializedProperty _initializeOnStart; private GUIStyle _headerStyle; @@ -27,9 +29,11 @@ private void OnEnable() _unityContextProp = serializedObject.FindProperty("UnityContext"); _tileCreatorProp = serializedObject.FindProperty("_tileCreatorBehaviour"); + _defaultTileMaterialProp = serializedObject.FindProperty("_defaultTileMaterial"); _tileProviderProp = serializedObject.FindProperty("TileProvider"); _dataFetcherProp = serializedObject.FindProperty("DataFetcher"); _cacheManagerProp = serializedObject.FindProperty("CacheManager"); + _locationFactoryProp = serializedObject.FindProperty("LocationFactory"); _initializeOnStart = serializedObject.FindProperty("InitializeOnStart"); } @@ -76,9 +80,10 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(_tileProviderProp, new GUIContent("Tile Provider")); EditorGUILayout.PropertyField(_dataFetcherProp, new GUIContent("Data Fetcher")); EditorGUILayout.PropertyField(_cacheManagerProp, new GUIContent("Cache Manager")); + EditorGUILayout.PropertyField(_locationFactoryProp, new GUIContent("Location Factory")); if (_tileCreatorProp == null && _tileProviderProp == null && _dataFetcherProp == null && - _cacheManagerProp == null) + _cacheManagerProp == null && _locationFactoryProp == null) { EditorGUILayout.HelpBox("Drag & drop your script components here.", MessageType.Info); } @@ -92,6 +97,7 @@ public override void OnInspectorGUI() if (!_overrideModulesFold) { EditorGUILayout.PropertyField(_initializeOnStart, new GUIContent("Initialize On Start")); + EditorGUILayout.PropertyField(_defaultTileMaterialProp, new GUIContent("Default Tile Material")); } } diff --git a/Runtime/Mapbox/Example/MapboxExamples.asmdef b/Runtime/Mapbox/Example/MapboxExamples.asmdef index debfa42b6..35e1e9a75 100644 --- a/Runtime/Mapbox/Example/MapboxExamples.asmdef +++ b/Runtime/Mapbox/Example/MapboxExamples.asmdef @@ -8,7 +8,8 @@ "GUID:36ca6af6e2b304d4090888554d6ce199", "GUID:1b64b068a01f943108c7f4b7a5356c7a", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:8ba268152fd3143f59445711358cb12f" + "GUID:8ba268152fd3143f59445711358cb12f", + "GUID:75469ad4d38634e559750d17036d5f7c" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/Example/Scripts/InputHandlers.cs b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs new file mode 100644 index 000000000..92af1af18 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs @@ -0,0 +1,247 @@ +using UnityEngine; +using UnityEngine.EventSystems; + +namespace Mapbox.Example.Scripts.MapInput +{ + /// + /// Mode-specific input handler. One implementation covers keyboard/mouse and + /// another covers touch; picks one at compile time based + /// on the build target. Each implementation covers the full vocabulary — methods + /// that don't make sense for the mode (e.g. secondary button on touch, two-finger + /// tilt on mouse) just return false / zero. + /// + internal interface IInputHandler + { + void Initialize(); + void UpdateState(); + + bool IsPointerOverUI(); + Vector3 GetPointerPosition(); + bool GetPointerDown(); + bool GetPointerHeld(); + bool GetSecondaryDown(); + bool GetSecondaryHeld(); + bool TryGetPinchZoomDelta(out float zoomDelta, float pinchSensitivity); + bool TryGetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity); + Vector3 GetZoomCenter(); + bool TouchCountDecreasedThisFrame { get; } + int GetTouchCount(); + } + + /// + /// Keyboard / mouse handler. Used on Editor and desktop standalone builds. + /// Touch-only concepts (touch count, pinch, tilt, etc.) are no-ops. + /// + internal sealed class MouseInputHandler : IInputHandler + { + private readonly IPointerInput _input; + + public MouseInputHandler(IPointerInput input) { _input = input; } + + public void Initialize() { /* legacy mouse needs no setup */ } + public void UpdateState() { /* mouse has no per-frame gesture mutex */ } + + public bool TouchCountDecreasedThisFrame => false; + public int GetTouchCount() => 0; + + public bool IsPointerOverUI() + { + if (EventSystem.current == null) return false; + return EventSystem.current.IsPointerOverGameObject(); + } + + public Vector3 GetPointerPosition() => _input.MousePosition; + public bool GetPointerDown() => _input.MouseLeftPressedThisFrame; + public bool GetPointerHeld() => _input.MouseLeftHeld; + public bool GetSecondaryDown() => _input.MouseRightPressedThisFrame; + public bool GetSecondaryHeld() => _input.MouseRightHeld; + + public bool TryGetPinchZoomDelta(out float zoomDelta, float pinchSensitivity) + { + // Scroll wheel; pinchSensitivity is unused on mouse — the wheel already + // emits per-notch deltas via IPointerInput.MouseScrollY. + var scrollY = _input.MouseScrollY; + if (Mathf.Abs(scrollY) > 0f) + { + zoomDelta = scrollY; + return true; + } + zoomDelta = 0f; + return false; + } + + public bool TryGetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity) + { + tiltDelta = 0f; + return false; + } + + public Vector3 GetZoomCenter() => _input.MousePosition; + } + + /// + /// Touch handler. Used on Android / iOS device builds. Mouse-only concepts + /// (secondary button, scroll wheel) are no-ops; pinch + tilt are recognised + /// via a per-frame two-finger gesture mutex run in . + /// + internal sealed class TouchInputHandler : IInputHandler + { + private readonly IPointerInput _input; + + // Per-frame two-finger gesture decision. Pinch vs tilt is exclusive; the + // winner is selected once per frame in UpdateState so the two query helpers + // (TryGetPinchZoomDelta / TryGetTwoFingerTiltDelta) read consistent state. + private enum Gesture { None, Pinch, Tilt } + private Gesture _gesture; + private float _pinchDelta; + private float _tiltAvgDeltaY; + + // Pinch state tracking. _previousTouch{0,1}Id let us detect 2→3→2 finger + // transitions where EnhancedTouch shifted which physical touches occupy + // slots [0..1] — without this, the next frame compares distance against + // a different pair and spikes a phantom zoom. + private float _previousPinchDistance; + private bool _pinchActive; + private int _previousTouchCount; + private int _previousTouch0Id = int.MinValue; + private int _previousTouch1Id = int.MinValue; + + public bool TouchCountDecreasedThisFrame { get; private set; } + + public TouchInputHandler(IPointerInput input) { _input = input; } + + public void Initialize() { _input.EnableTouchSupport(); } + public int GetTouchCount() => _input.TouchCount; + + public bool IsPointerOverUI() + { + if (EventSystem.current == null) return false; + if (_input.TouchCount > 0) + return EventSystem.current.IsPointerOverGameObject(_input.GetTouchPointerId(0)); + return EventSystem.current.IsPointerOverGameObject(); + } + + public Vector3 GetPointerPosition() + { + return _input.TouchCount > 0 ? (Vector3)_input.GetTouchPosition(0) : Vector3.zero; + } + + public bool GetPointerDown() + { + if (_input.TouchCount <= 0) return false; + return _input.GetTouchPhase(0) == PointerTouchPhase.Began; + } + + public bool GetPointerHeld() + { + if (_input.TouchCount != 1) return false; + var phase = _input.GetTouchPhase(0); + return phase == PointerTouchPhase.Moved || phase == PointerTouchPhase.Stationary; + } + + // Touch has no secondary pointer; the camera's rotate-via-secondary code + // (SlippyMapCamera right-click rotation, Moving3dCamera right-click pitch+yaw) + // stays dormant on this handler. + public bool GetSecondaryDown() => false; + public bool GetSecondaryHeld() => false; + + public bool TryGetPinchZoomDelta(out float zoomDelta, float pinchSensitivity) + { + if (_gesture == Gesture.Pinch) + { + zoomDelta = _pinchDelta / Mathf.Max(Screen.height, 1) * pinchSensitivity; + return true; + } + zoomDelta = 0f; + return false; + } + + public bool TryGetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity) + { + if (_gesture == Gesture.Tilt) + { + tiltDelta = _tiltAvgDeltaY / Mathf.Max(Screen.height, 1) * tiltSensitivity; + return true; + } + tiltDelta = 0f; + return false; + } + + public Vector3 GetZoomCenter() + { + if (_input.TouchCount >= 2) + { + Vector3 t0 = _input.GetTouchPosition(0); + Vector3 t1 = _input.GetTouchPosition(1); + return (t0 + t1) / 2f; + } + return Vector3.zero; + } + + public void UpdateState() + { + var touchCount = _input.TouchCount; + TouchCountDecreasedThisFrame = touchCount < _previousTouchCount; + _previousTouchCount = touchCount; + + _gesture = Gesture.None; + _pinchDelta = 0f; + _tiltAvgDeltaY = 0f; + + if (touchCount < 2) + { + _pinchActive = false; + _previousTouch0Id = int.MinValue; + _previousTouch1Id = int.MinValue; + return; + } + + var touch0Pos = _input.GetTouchPosition(0); + var touch1Pos = _input.GetTouchPosition(1); + var touch0DeltaY = _input.GetTouchDeltaY(0); + var touch1DeltaY = _input.GetTouchDeltaY(1); + var t0Id = _input.GetTouchId(0); + var t1Id = _input.GetTouchId(1); + var currentDistance = Vector2.Distance(touch0Pos, touch1Pos); + + bool pairChanged = !_pinchActive || + t0Id != _previousTouch0Id || t1Id != _previousTouch1Id; + if (pairChanged) + { + _pinchActive = true; + _previousPinchDistance = currentDistance; + _previousTouch0Id = t0Id; + _previousTouch1Id = t1Id; + return; + } + + var pinchDelta = currentDistance - _previousPinchDistance; + _previousPinchDistance = currentDistance; + + var pinchMag = Mathf.Abs(pinchDelta); + var avgDeltaY = (touch0DeltaY + touch1DeltaY) / 2f; + var tiltMag = Mathf.Abs(avgDeltaY); + var bothFingersSameDirection = touch0DeltaY * touch1DeltaY > 0f; + + // Normalize both magnitudes to Screen.height so the thresholds are DPI- + // independent. MinFrac ≈ 1 pixel on a 1080-height screen. Pinch wins + // ties (pinchFrac >= tiltFrac) to avoid "None" frames when magnitudes + // are exactly equal (common on near-pure vertical drags). + var screenH = Mathf.Max(Screen.height, 1); + var pinchFrac = pinchMag / screenH; + var tiltFrac = tiltMag / screenH; + const float MinFrac = 1f / 1080f; + + if (pinchFrac > MinFrac && pinchFrac >= tiltFrac) + { + _gesture = Gesture.Pinch; + _pinchDelta = pinchDelta; + } + else if (bothFingersSameDirection && tiltFrac > MinFrac) + { + _gesture = Gesture.Tilt; + _tiltAvgDeltaY = avgDeltaY; + } + } + } +} diff --git a/Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta new file mode 100644 index 000000000..3ccf3bb0c --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/InputHandlers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f2e420aa19dd41bbb0d0e579bc3d350 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs index 72f0a4601..1cc4a2f23 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/MapboxMapBehaviour.cs @@ -1,16 +1,17 @@ using System; +using System.Collections; using System.Linq; using Mapbox.BaseModule; using Mapbox.BaseModule.Data.DataFetchers; -using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Platform.Cache; using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.ModuleBehaviours; using Mapbox.Example.Scripts.TileProviderBehaviours; using Mapbox.ImageModule.Terrain.TerrainStrategies; +using Mapbox.LocationModule; using Mapbox.UnityMapService; using Mapbox.UnityMapService.TileProviders; using UnityEngine; @@ -23,51 +24,66 @@ public class MapboxMapBehaviour : MapBehaviourCore public UnityContext UnityContext; [SerializeField] protected TileCreatorBehaviour _tileCreatorBehaviour; + [Tooltip("Material used when no TileCreatorBehaviour is assigned. Asset reference keeps the shader alive in player builds. If you swap in a TileCreatorBehaviour, this field is ignored.")] + [SerializeField] protected Material _defaultTileMaterial; [SerializeField] protected TileProviderBehaviour TileProvider; [SerializeField] protected DataFetchingManagerBehaviour DataFetcher; [SerializeField] protected MapboxCacheManagerBehaviour CacheManager; + [SerializeField] protected LocationProviderFactory LocationFactory; private MapService _mapService; public bool InitializeOnStart = true; public Action MapServiceReady = (v) => { }; + public virtual void Start() { if (InitializeOnStart) - Initialize(); + StartCoroutine(Initialize()); } - + [ContextMenu("Initialize")] - public override void Initialize() + public override IEnumerator Initialize() { if (InitializationStatus != InitializationStatus.WaitingForInitialization) - return; + yield break; MapInformation.Initialize(); - UnityContext.Initialize(); + + yield return UnityContext.Initialize(); + //we handle permission via unity, instead of using location providers themselves + yield return UnityContext.HandlePermission(); + + if (Application.isEditor || UnityContext.LocationPermissionState == LocationPermissionState.Granted) + { + if (LocationFactory != null) + { + yield return LocationFactory.Initialize(); + var locationProvider = LocationFactory.DefaultLocationProvider; + MapInformation.SetLatitudeLongitude(locationProvider.CurrentLocation.LatitudeLongitude); + } + } + else + { + Debug.Log("Location permission is " + UnityContext.LocationPermissionState); + } var mapboxContext = new MapboxContext(); + yield return mapboxContext.Initialize(); _mapService = GetMapService(mapboxContext, UnityContext); MapServiceReady(_mapService); - + MapboxMap = CreateMapObject(); MapboxMap.Initialized += InitializationCompleted; - StartCoroutine(MapboxMap.Initialize()); + yield return MapboxMap.Initialize(); } + private void InitializationCompleted() { Initialized(MapboxMap); MapboxMap.LoadMapView(); } - - private void Update() - { - if (InitializationStatus == InitializationStatus.ReadyForUpdates && _mapService.IsReady()) - { - MapboxMap.MapUpdated(); - } - } private void OnValidate() { @@ -84,8 +100,6 @@ private void OnDestroy() MapboxMap?.OnDestroy(); UnityContext.OnDestroy(); } - - protected virtual MapboxMap CreateMapObject() { @@ -110,8 +124,19 @@ protected virtual MapboxMapVisualizer CreateMapVisualizer(IMapInformation mapInf } else { - var defaultMapboxTerrainMaterial = new Material(Shader.Find(Constants.Map.DefaultTerrainShaderName)); - tileCreator = new TileCreator(unityContext, new[] { defaultMapboxTerrainMaterial }); + // Fallback when no TileCreatorBehaviour is assigned on the GameObject. + // The Material is taken from a direct asset reference (_defaultTileMaterial) + // so its shader survives player-build shader stripping — Shader.Find can't + // resolve shaders that aren't referenced from a shipped Material or listed + // in Project Settings → Graphics → Always Included Shaders. + if (_defaultTileMaterial == null) + { + throw new InvalidOperationException( + $"MapboxMapBehaviour on '{name}' has no TileCreator assigned and no default tile Material set. " + + "Assign a TileCreatorBehaviour on this GameObject, or drag a Material (e.g. ElevatedTerrainMaterial) " + + "into the 'Default Tile Material' field on MapboxMapBehaviour."); + } + tileCreator = new TileCreator(unityContext, new[] { _defaultTileMaterial }); } return new MapboxMapVisualizer(mapInfo, unityContext, tileCreator); } diff --git a/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs b/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs index 40c811081..12a50a15f 100644 --- a/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs +++ b/Runtime/Mapbox/Example/Scripts/Map/SnapMapToTransform.cs @@ -20,7 +20,7 @@ void Update() if (_map != null && _map.Status >= InitializationStatus.ReadyForUpdates) { var latlng = _map.MapInformation.ConvertPositionToLatLng(Transform.position); - _map.MapInformation.SetInformation(latlng); + _map.ChangeView(latlng); } } } diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs new file mode 100644 index 000000000..c68efc7db --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs @@ -0,0 +1,79 @@ +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.Example.Scripts.Map; +using UnityEngine; + +namespace Mapbox.Example.Scripts.MapInput +{ + /// + /// Generic base MonoBehaviour for any MapInput-based camera. + /// Handles initialization, the Update loop, and ChangeView dispatch. + /// Subclass with a concrete camera type, or use directly if no extra behaviour is needed. + /// + public abstract class MapCameraBehaviour : MonoBehaviour where T : MapInput + { + [Tooltip("Reference to the map behaviour that provides map state and tile management")] + public MapBehaviourCore MapBehaviour; + [Tooltip("Camera used for map rendering and input raycasting. Falls back to Camera.main if not set")] + public Camera Camera; + + protected MapboxMap Map; + protected bool IsInitialized; + + public abstract T Core { get; } + + protected virtual void Awake() + { + if (MapBehaviour == null) MapBehaviour = FindObjectOfType(); + if (Camera == null) Camera = Camera.main; + + if (MapBehaviour == null) + { + Debug.LogError($"{GetType().Name} could not find a MapBehaviourCore in the scene. Assign the MapBehaviour field or add a Mapbox map to the scene; the camera will not initialize.", this); + enabled = false; + return; + } + + MapBehaviour.Initialized += OnMapInitialized; + } + + protected virtual void OnDestroy() + { + if (MapBehaviour != null) + MapBehaviour.Initialized -= OnMapInitialized; + + // Tear down the camera core's IMapInformation subscriptions. MapInformation + // can outlive the behaviour (DontDestroyOnLoad / scene reload); without the + // detach, the camera core and its captured _camera Transform stay rooted. + if (IsInitialized && Map != null) + { + Core?.Teardown(Map.MapInformation); + } + } + + protected virtual void OnMapInitialized(MapboxMap map) + { + // Defensive: if MapBehaviour.Initialized fires twice (re-init, swapped map + // asset), unsubscribe from the previous MapInformation before binding to + // the new one. Otherwise the prior subscriptions leak until OnDestroy. + if (IsInitialized && Map != null && Map.MapInformation != null) + { + Core?.Teardown(Map.MapInformation); + } + + Map = map; + IsInitialized = true; + Core.Initialize(Camera, map.MapInformation); + } + + protected virtual void Update() + { + if (!IsInitialized || Map.MapInformation == null) + return; + + var output = Core.UpdateCamera(Map.MapInformation); + if (output.HasChanged) + Map.ChangeView(output.Center, output.Zoom, output.Pitch, output.Bearing, output.Scale); + } + } +} diff --git a/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta new file mode 100644 index 000000000..e32bec822 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/MapCameraBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 128117eb38e334d748d030fded176dda +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Scripts/MapInput.cs b/Runtime/Mapbox/Example/Scripts/MapInput.cs index b1a7e7614..f783858bf 100644 --- a/Runtime/Mapbox/Example/Scripts/MapInput.cs +++ b/Runtime/Mapbox/Example/Scripts/MapInput.cs @@ -1,11 +1,163 @@ +using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; using UnityEngine; namespace Mapbox.Example.Scripts.MapInput { + /// + /// Reusable output from UpdateCamera. Populated each frame by the camera implementation. + /// Passed to MapboxMap.ChangeView — null fields mean "no change". + /// + public class CameraOutput + { + public LatitudeLongitude? Center; + public float? Zoom; + public float? Pitch; + public float? Bearing; + public float? Scale; + public bool HasChanged; + + public void Reset() + { + Center = null; + Zoom = null; + Pitch = null; + Bearing = null; + Scale = null; + HasChanged = false; + } + } + public abstract class MapInput { protected Camera _camera; - public abstract bool UpdateCamera(IMapInformation mapInfo); + protected Plane _controlPlane = new Plane(Vector3.up, Vector3.zero); + protected readonly CameraOutput _output = new CameraOutput(); + + // Per-platform handler. Editor and standalone builds use the mouse handler; + // Android/iOS device builds use the touch handler. The split is compile-time + // (no runtime detection) — each platform gets a single-purpose input path + // instead of a unified shim that has to cover both worlds at once. +#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR + private readonly IInputHandler _handler = new TouchInputHandler(new PointerInput()); +#else + private readonly IInputHandler _handler = new MouseInputHandler(new PointerInput()); +#endif + + public virtual void Initialize(Camera camera, IMapInformation mapInfo) + { + _camera = camera ? camera : Camera.main; + _handler.Initialize(); + } + + /// + /// Called from the owning behaviour's OnDestroy. Override to unsubscribe from + /// events you wired in — + /// IMapInformation can outlive the camera (DontDestroyOnLoad / scene reload), + /// and event closures otherwise root the destroyed camera + its Camera Transform. + /// + public virtual void Teardown(IMapInformation mapInfo) { } + + public abstract CameraOutput UpdateCamera(IMapInformation mapInfo); + + #region Plane Intersection + + protected bool GetPlaneIntersection(Vector3 screenPosition, out Vector3 hit) + { + return _camera.GetPlaneIntersection(_controlPlane, screenPosition, out hit); + } + + #endregion + + #region Camera Utilities + + public Plane[] GetFrustumPlanes() + { + return GeometryUtility.CalculateFrustumPlanes(_camera); + } + + public Transform GetTransform() + { + return _camera.transform; + } + + #endregion + + #region Value Clamping + + // Absolute Mercator pyramid max. Mirrors MapboxMapVisualizer.MaxMercatorZoom + // (`int 22`) — kept here as a float const because C# default-parameter values + // must be compile-time constants of the parameter's exact type. If the pyramid + // cap ever changes, update both this and MapboxMapVisualizer.MaxMercatorZoom. + public const float MaxClampZoom = 22f; + + protected static float ClampZoom(float zoom, float min = 0f, float max = MaxClampZoom) + { + return Mathf.Clamp(zoom, min, max); + } + + protected static float ClampPitch(float pitch, float min = 15f, float max = 90f) + { + return Mathf.Clamp(pitch, min, max); + } + + protected static float ClampBearing(float bearing) + { + bearing %= 360f; + if (bearing > 180f) bearing -= 360f; + if (bearing < -180f) bearing += 360f; + return bearing; + } + + #endregion + + #region Input Helpers + + // All input reads delegate to _handler. Touch-only state (pinch / tilt + // mutex, touch-pair tracking) lives entirely inside TouchInputHandler; + // mouse-only state (RMB held) lives entirely inside MouseInputHandler. + + /// + /// Call at the start of UpdateCamera to maintain input state between frames. + /// On touch: resets pinch tracking, runs the per-frame pinch/tilt decision. + /// On mouse: no-op. + /// + protected void UpdateInputState() => _handler.UpdateState(); + + /// + /// True on the frame the active touch count drops (e.g. 2→1 when one finger + /// lifts during a pinch). Cameras should treat this as a fresh drag start so + /// _dragOrigin is reset to the surviving finger's position. Always false on + /// the mouse handler. + /// + protected bool TouchCountDecreasedThisFrame => _handler.TouchCountDecreasedThisFrame; + + private int GetTouchCount() => _handler.GetTouchCount(); + + protected bool IsPointerOverUI() => _handler.IsPointerOverUI(); + protected Vector3 GetPointerPosition() => _handler.GetPointerPosition(); + protected bool GetPointerDown() => _handler.GetPointerDown(); + protected bool GetPointerHeld() => _handler.GetPointerHeld(); + protected bool GetSecondaryDown() => _handler.GetSecondaryDown(); + protected bool GetSecondaryHeld() => _handler.GetSecondaryHeld(); + + /// + /// Returns true if zoom input was detected this frame. On touch: from the + /// pinch gesture cached by UpdateState. On mouse: from the scroll wheel. + /// + protected bool GetPinchZoomDelta(out float zoomDelta, float pinchSensitivity = 5f) + => _handler.TryGetPinchZoomDelta(out zoomDelta, pinchSensitivity); + + /// + /// Returns true if a two-finger vertical-tilt gesture was detected this frame. + /// Always false on the mouse handler. + /// + protected bool GetTwoFingerTiltDelta(out float tiltDelta, float tiltSensitivity = 0.5f) + => _handler.TryGetTwoFingerTiltDelta(out tiltDelta, tiltSensitivity); + + protected Vector3 GetZoomCenter() => _handler.GetZoomCenter(); + + #endregion } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs index 88c724b10..7491410bb 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCamera.cs @@ -1,112 +1,149 @@ using System; using Mapbox.BaseModule.Map; using UnityEngine; -using UnityEngine.EventSystems; +using UnityEngine.Serialization; namespace Mapbox.Example.Scripts.MapInput { [Serializable] public class Moving3dCamera : MapInput { + [Tooltip("Camera tilt angle. 90 = top-down, 15 = near-horizon")] [Range(15, 90)] public float Pitch; + [Tooltip("Camera rotation in degrees. 0 = north up")] [Range(-180, 180)] public float Bearing; [NonSerialized] public float ZoomValue; - public float Speed = -10; - public float CameraDistance; - - public Action Updated = () => { }; + [NonSerialized] public float CameraDistance; + + [Tooltip("Animation curve mapping zoom level to camera distance. X axis = zoom, Y axis = distance")] + public AnimationCurveContainer CameraCurve; + [Tooltip("Scales camera distance from curve output. Use to adjust height without re-authoring the curve. Default: 2")] + [FormerlySerializedAs("CamDistanceMultiplier")] + public float DistanceScale = 2; + [Tooltip("Scroll wheel zoom sensitivity per step. Default: 0.25")] + [FormerlySerializedAs("ZoomSpeed")] + public float ZoomSensitivity = 0.25f; + [Tooltip("Right-click rotation sensitivity. Higher = faster rotation. Default: 50")] + public float RotationSpeed = 50.0f; private Vector3 _previousScreenPosition; private Vector3 _dragOrigin; - [SerializeField] private Vector3 _targetPosition; - private float deltaAngleH; - private float deltaAngleV; - public float RotationSpeed = 50.0f; - public AnimationCurveContainer CameraCurve; - public float CamDistanceMultiplier = 2; - public float ZoomSpeed = 0.25f; - - - public void Initialize(Camera camera, IMapInformation start) + private Vector3 _targetPosition; + + // Stored handlers so Teardown can detach from the IMapInformation events. + // Without these, the closures root the camera (and _camera Transform) beyond + // the owning behaviour's destruction. + private Action _onLatLngChanged; + private Action _onViewChanged; + + + public override void Initialize(Camera camera, IMapInformation start) { - _camera = camera ? camera : Camera.main; + base.Initialize(camera, start); Pitch = start.Pitch; Bearing = start.Bearing; ZoomValue = start.Zoom; - SetCamPositionByMapInfo(start); - start.LatitudeLongitudeChanged += information => + SetCamera(start); + _onLatLngChanged = information => { _targetPosition = Vector3.zero; SetCamera(information); }; - start.ViewChanged += SetCamera; + _onViewChanged = SetCamera; + start.LatitudeLongitudeChanged += _onLatLngChanged; + start.ViewChanged += _onViewChanged; } - - public override bool UpdateCamera(IMapInformation mapInformation) + + public override void Teardown(IMapInformation mapInfo) { - if (EventSystem.current.IsPointerOverGameObject()) - return false; - - var hasChanged = false; - if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) + if (mapInfo == null) return; + if (_onLatLngChanged != null) mapInfo.LatitudeLongitudeChanged -= _onLatLngChanged; + if (_onViewChanged != null) mapInfo.ViewChanged -= _onViewChanged; + _onLatLngChanged = null; + _onViewChanged = null; + } + + public override CameraOutput UpdateCamera(IMapInformation mapInformation) + { + _output.Reset(); + UpdateInputState(); + + if (IsPointerOverUI()) + return _output; + + var pointerPos = GetPointerPosition(); + Vector3 cursorHit; + if (!GetPlaneIntersection(pointerPos, out cursorHit)) + return _output; + + if (GetPointerDown() || GetSecondaryDown() || TouchCountDecreasedThisFrame) { - _previousScreenPosition = UnityEngine.Input.mousePosition; - _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); + _previousScreenPosition = pointerPos; + _dragOrigin = cursorHit; } - if (Input.GetMouseButton(0)) + if (GetPointerHeld()) { - var newPoint = GetPlaneIntersection(Input.mousePosition); + if (!GetPlaneIntersection(pointerPos, out var newPoint)) + return _output; Vector3 pos = newPoint - _dragOrigin; - Vector3 move = new Vector3(pos.x * Speed, 0, pos.z * Speed); - - _targetPosition += move; - hasChanged = true; - + // Skip the redraw when the held pointer didn't actually move (touch Stationary, + // mouse-held-still). HasChanged=true here would trigger a full ChangeView+Load. + if (pos.x != 0f || pos.z != 0f) + { + _targetPosition -= new Vector3(pos.x, 0, pos.z); + _output.HasChanged = true; + } } - else if (Input.GetMouseButton(1) ) + else if (GetSecondaryHeld()) { - var deltaMousePos = (Input.mousePosition - _previousScreenPosition); - deltaAngleH = deltaMousePos.x; - deltaAngleV = deltaMousePos.y; + var deltaMousePos = (pointerPos - _previousScreenPosition); + var deltaAngleH = deltaMousePos.x; + var deltaAngleV = deltaMousePos.y; if (deltaAngleH != 0 || deltaAngleV != 0) { Pitch -= deltaAngleV * Time.deltaTime * RotationSpeed; - Pitch = Mathf.Min(90, Mathf.Max(15, Pitch)); - Bearing += deltaAngleH * Time.deltaTime * RotationSpeed; + Pitch = ClampPitch(Pitch); + Bearing = ClampBearing(Bearing + deltaAngleH * Time.deltaTime * RotationSpeed); + _output.HasChanged = true; } - hasChanged = true; } - else if (Input.mouseScrollDelta.magnitude > 0) + else if (GetPinchZoomDelta(out var zoomDelta)) { - Zoom( - mapInformation, - GetPlaneIntersection(Input.mousePosition), - Input.GetAxis("Mouse ScrollWheel")); - //we still update _targetPosition with center screen as if this cursor focused zoom didn't happen - //this'll ensure smooth transition to other movements (pan after zoom) - hasChanged = true; + var zoomCenter = GetZoomCenter(); + if (GetPlaneIntersection(zoomCenter, out var zoomHit)) + { + Zoom(mapInformation, zoomHit, zoomDelta); + _output.HasChanged = true; + } + } + + if (GetTwoFingerTiltDelta(out var tiltDelta)) + { + Pitch -= tiltDelta * RotationSpeed; + Pitch = ClampPitch(Pitch); + _output.HasChanged = true; } - //SetCamPositionByMapInfo(mapInformation); - - _dragOrigin = GetPlaneIntersection(Input.mousePosition); - _previousScreenPosition = Input.mousePosition; - - //we probably shouldn't write to mapInformation here but do it in Moving3dCamBehaviour, along with pitch and bearing - //mapInformation.ViewCenter = GetPlaneIntersection(Camera.ViewportToScreenPoint(new Vector3(0.5f, 0.5f))); - return hasChanged; + GetPlaneIntersection(pointerPos, out _dragOrigin); + _previousScreenPosition = pointerPos; + + if (_output.HasChanged) + { + SetCamPositionByMapInfo(); + _output.Zoom = ZoomValue; + _output.Pitch = Pitch; + _output.Bearing = Bearing; + } + return _output; } public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAction) { - //calculate next zoom value by simply adding scroll delta * speed - //var mouseWorld = position; - //var mouseMeter = mouseWorld * mapInformation.Scale; - var postZoom = ZoomValue + zoomAction * ZoomSpeed; + var postZoom = ClampZoom(ZoomValue + zoomAction * ZoomSensitivity); //to be able to achieve zoom on mouse cursor, we have to move camera on mouse world pos - camera pos line (b-c) //but our camera distance uses camera target (mid screen) to camera pos distance (a-c) @@ -129,55 +166,68 @@ public void Zoom(IMapInformation mapInformation, Vector3 position, float zoomAct var preDistance = CalculateCameraDistance(mapInformation, ZoomValue); var camDistanceToMouse = Vector3.Distance(_camera.transform.position, position); ZoomValue = postZoom; - var postDistance = CalculateCameraDistance(mapInformation, postZoom); - var newCamDistanceToMouse = camDistanceToMouse * (postDistance / preDistance); CameraDistance = CalculateCameraDistance(mapInformation, ZoomValue); - _targetPosition = Vector3.LerpUnclamped(postZoomTarget, postZoomPos, (camDistanceToMouse - newCamDistanceToMouse) / camDistanceToMouse); + + // Guard against div-by-zero AND non-finite Scale: top-down orthographic, + // first-frame state, a camera curve that evaluates to 0, or mapInformation.Scale + // being 0 (early init) can make CalculateCameraDistance return Inf, which + // passes a MinDistance check but later turns into Inf - Inf = NaN in the lerp. + // Fall back to a straight target snap when the cursor-zoom math has nothing + // meaningful to interpolate. + const float MinDistance = 0.0001f; + if (preDistance <= MinDistance || camDistanceToMouse <= MinDistance || + !float.IsFinite(preDistance) || !float.IsFinite(CameraDistance)) + { + _targetPosition = postZoomTarget; + return; + } + + var postDistance = CameraDistance; + var newCamDistanceToMouse = camDistanceToMouse * (postDistance / preDistance); + var lerped = Vector3.LerpUnclamped(postZoomTarget, postZoomPos, (camDistanceToMouse - newCamDistanceToMouse) / camDistanceToMouse); + // Belt-and-suspenders: if anything snuck a non-finite component through, + // keep _targetPosition stable instead of corrupting it. + if (float.IsFinite(lerped.x) && float.IsFinite(lerped.y) && float.IsFinite(lerped.z)) + { + _targetPosition = lerped; + } + else + { + _targetPosition = postZoomTarget; + } } - private void SetCamPositionByMapInfo(IMapInformation start) + private void SetCamPositionByMapInfo() { - CameraDistance = CalculateCameraDistance(start, start.Zoom); _camera.transform.position = _targetPosition; _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); - _dragOrigin = GetPlaneIntersection(UnityEngine.Input.mousePosition); + GetPlaneIntersection(GetPointerPosition(), out _dragOrigin); } public void SetCamera(IMapInformation mapInfo) { Pitch = mapInfo.Pitch; Bearing = mapInfo.Bearing; - SetCamPositionByMapInfo(mapInfo); - } - private Vector3 GetPlaneIntersection(Vector3 screenPosition) - { - var ray = _camera.ScreenPointToRay(screenPosition); - var dirNorm = ray.direction / ray.direction.y; - var intersectionPos = ray.origin - dirNorm * ray.origin.y; - return intersectionPos; + CameraDistance = CalculateCameraDistance(mapInfo, mapInfo.Zoom); + _camera.transform.position = _targetPosition; + _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); + _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); + GetPlaneIntersection(GetPointerPosition(), out _dragOrigin); } - + private float CalculateCameraDistance(IMapInformation mapInformation, float postZoom) { var distance = CameraCurve.Evaluate(postZoom); - return CamDistanceMultiplier * distance / mapInformation.Scale; + return DistanceScale * distance / mapInformation.Scale; } public Vector3 GetViewCenterPosition() { - return GetPlaneIntersection(_camera.ViewportToScreenPoint(new Vector3(.5f, .5f, 0f))); - } - - public Plane[] GetFrustrumPlanes() - { - return GeometryUtility.CalculateFrustumPlanes(_camera); + GetPlaneIntersection(_camera.ViewportToScreenPoint(new Vector3(.5f, .5f, 0f)), out var center); + return center; } - public Transform GetTransform() - { - return _camera.transform; - } } } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs index 486bba892..937618eb4 100644 --- a/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/Moving3dCameraBehaviour.cs @@ -1,53 +1,30 @@ using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; using UnityEngine; +using UnityEngine.Serialization; namespace Mapbox.Example.Scripts.MapInput { - public class Moving3dCameraBehaviour : MonoBehaviour + public class Moving3dCameraBehaviour : MapCameraBehaviour { - public MapBehaviourCore MapBehaviour; - public Camera Camera; - public Moving3dCamera Core; + [Tooltip("3D camera settings. Camera moves through world space while the map stays in place")] + [FormerlySerializedAs("Core")] + [SerializeField] private Moving3dCamera _core; - private MapboxMap _map; - private bool _isInitialized = false; - - private void Awake() - { - if (MapBehaviour == null) MapBehaviour = FindObjectOfType(); - if(Camera == null) Camera = Camera.main; - - MapBehaviour.Initialized += (map) => - { - _map = map; - _isInitialized = true; - Core.Initialize(Camera, _map.MapInformation); - }; - } - - public void Update() - { - if (_isInitialized && _map.MapInformation != null && Core.UpdateCamera(_map.MapInformation)) - { - _map.MapInformation.SetInformation(null, Core.ZoomValue, Core.Pitch, Core.Bearing); - } - } + public override Moving3dCamera Core => _core; public Vector3 GetViewCenterPosition() { - return Core.GetViewCenterPosition(); + return _core.GetViewCenterPosition(); } public void Zoom(MapInformation mapInformation, Vector3 point, float zoomAction) { - Core.Zoom(mapInformation, point, zoomAction); + _core.Zoom(mapInformation, point, zoomAction); } - + public void Zoom(MapInformation mapInformation, float zoomAction) { - Core.Zoom(mapInformation, GetViewCenterPosition(), zoomAction); + _core.Zoom(mapInformation, GetViewCenterPosition(), zoomAction); } } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/Example/Scripts/PointerInput.cs b/Runtime/Mapbox/Example/Scripts/PointerInput.cs new file mode 100644 index 000000000..af28a2fe8 --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/PointerInput.cs @@ -0,0 +1,152 @@ +using UnityEngine; +#if ENABLE_INPUT_SYSTEM +using UnityEngine.InputSystem; +using NewTouch = UnityEngine.InputSystem.EnhancedTouch.Touch; +using NewTouchPhase = UnityEngine.InputSystem.TouchPhase; +#endif + +namespace Mapbox.Example.Scripts.MapInput +{ + /// + /// Backend-agnostic touch phase. Both legacy UnityEngine.TouchPhase and + /// UnityEngine.InputSystem.TouchPhase are mapped onto this enum so the + /// MapInput call-sites don't need to know which input system is compiled in. + /// + internal enum PointerTouchPhase + { + None, + Began, + Moved, + Stationary, + Ended, + Canceled + } + + /// + /// Thin abstraction over Unity's two input backends. + /// has exactly one of two implementations compiled in, selected by Unity's + /// built-in ENABLE_INPUT_SYSTEM define (set when Active Input Handling + /// in Player Settings is "New" or "Both"). Gating on ENABLE_INPUT_SYSTEM + /// rather than package presence is intentional: com.unity.inputsystem is + /// a hard dependency of this package, so it's always installed, but existing + /// legacy-input projects must keep running the #else branch until the + /// user explicitly opts in via Active Input Handling. MapInput.cs talks only + /// to this interface, so the rest of the camera/input layer stays free of + /// #if branching. + /// + internal interface IPointerInput + { + /// + /// Called once from MapInput.Initialize. Legacy is a no-op; new-input + /// path enables EnhancedTouchSupport. + /// + void EnableTouchSupport(); + + int TouchCount { get; } + Vector2 GetTouchPosition(int index); + float GetTouchDeltaY(int index); + int GetTouchId(int index); + PointerTouchPhase GetTouchPhase(int index); + /// + /// Identifier suitable for EventSystem.IsPointerOverGameObject(int). + /// Returns finger.index on the new system and fingerId on legacy + /// — both are what their respective EventSystem expects. + /// + int GetTouchPointerId(int index); + + Vector3 MousePosition { get; } + bool MouseLeftPressedThisFrame { get; } + bool MouseLeftHeld { get; } + bool MouseRightPressedThisFrame { get; } + bool MouseRightHeld { get; } + /// + /// Scroll-wheel delta normalized so both backends feed approximately the same + /// magnitude into ZoomSensitivity (~0.1 per Windows notch). Returns 0 when not + /// scrolling and when the mouse device is unavailable. + /// + float MouseScrollY { get; } + } + +#if ENABLE_INPUT_SYSTEM + internal sealed class PointerInput : IPointerInput + { + public void EnableTouchSupport() + { + if (!UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.enabled) + UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.Enable(); + } + + public int TouchCount => NewTouch.activeTouches.Count; + public Vector2 GetTouchPosition(int index) => NewTouch.activeTouches[index].screenPosition; + public float GetTouchDeltaY(int index) => NewTouch.activeTouches[index].delta.y; + public int GetTouchId(int index) => NewTouch.activeTouches[index].touchId; + public int GetTouchPointerId(int index) => NewTouch.activeTouches[index].finger.index; + + public PointerTouchPhase GetTouchPhase(int index) + { + switch (NewTouch.activeTouches[index].phase) + { + case NewTouchPhase.Began: return PointerTouchPhase.Began; + case NewTouchPhase.Moved: return PointerTouchPhase.Moved; + case NewTouchPhase.Stationary: return PointerTouchPhase.Stationary; + case NewTouchPhase.Ended: return PointerTouchPhase.Ended; + case NewTouchPhase.Canceled: return PointerTouchPhase.Canceled; + default: return PointerTouchPhase.None; + } + } + + public Vector3 MousePosition => + Mouse.current != null ? (Vector3)Mouse.current.position.ReadValue() : Vector3.zero; + + public bool MouseLeftPressedThisFrame => + Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame; + public bool MouseLeftHeld => + Mouse.current != null && Mouse.current.leftButton.isPressed; + + public bool MouseRightPressedThisFrame => + Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame; + public bool MouseRightHeld => + Mouse.current != null && Mouse.current.rightButton.isPressed; + + // Raw scroll.y is ~120 per notch on Windows; legacy Input.GetAxis returns + // ~0.1 per notch. /1200f normalizes to the legacy scale so both backends feed + // the same magnitude into ZoomSensitivity. + public float MouseScrollY => + Mouse.current != null ? Mouse.current.scroll.ReadValue().y / 1200f : 0f; + } +#else + internal sealed class PointerInput : IPointerInput + { + public void EnableTouchSupport() { /* legacy touch is auto-enabled */ } + + public int TouchCount => Input.touchCount; + public Vector2 GetTouchPosition(int index) => Input.GetTouch(index).position; + public float GetTouchDeltaY(int index) => Input.GetTouch(index).deltaPosition.y; + public int GetTouchId(int index) => Input.GetTouch(index).fingerId; + public int GetTouchPointerId(int index) => Input.GetTouch(index).fingerId; + + public PointerTouchPhase GetTouchPhase(int index) + { + switch (Input.GetTouch(index).phase) + { + case TouchPhase.Began: return PointerTouchPhase.Began; + case TouchPhase.Moved: return PointerTouchPhase.Moved; + case TouchPhase.Stationary: return PointerTouchPhase.Stationary; + case TouchPhase.Ended: return PointerTouchPhase.Ended; + case TouchPhase.Canceled: return PointerTouchPhase.Canceled; + default: return PointerTouchPhase.None; + } + } + + public Vector3 MousePosition => Input.mousePosition; + public bool MouseLeftPressedThisFrame => Input.GetMouseButtonDown(0); + public bool MouseLeftHeld => Input.GetMouseButton(0); + public bool MouseRightPressedThisFrame => Input.GetMouseButtonDown(1); + public bool MouseRightHeld => Input.GetMouseButton(1); + + // Input.GetAxis("Mouse ScrollWheel") returns 0 when not scrolling, so we + // don't need the historic Mathf.Abs(mouseScrollDelta) > 0 short-circuit. + public float MouseScrollY => Input.GetAxis("Mouse ScrollWheel"); + } +#endif +} diff --git a/Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta b/Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta new file mode 100644 index 000000000..d2d238eed --- /dev/null +++ b/Runtime/Mapbox/Example/Scripts/PointerInput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6133b58d9727428fbbc3e26c59a19b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs index eb5fb0c2f..bff83d749 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCamera.cs @@ -18,54 +18,74 @@ public enum RotationMode [Serializable] private class RotationSettings { + [Tooltip("Enable right-click drag to rotate the camera or map")] public bool Enabled = true; + [Tooltip("Rotation sensitivity. Higher values = faster rotation. Default: 50")] public float Speed = 50.0f; + [Tooltip("RotateTheCamera: camera orbits the map. RotateTheMap: map rotates under a fixed camera")] public RotationMode rotationMode; + [Tooltip("Root transform of the map. Auto-detected from MapBehaviourCore if left empty")] public Transform MapRoot; } - + [Serializable] private class ZoomSettings { + [Tooltip("Enable scroll wheel to zoom in/out")] public bool Enabled = true; + [Tooltip("Zoom sensitivity per scroll step. Default: 0.25")] public float Speed = 0.25f; + [Tooltip("When enabled, zoom centers on cursor position. When disabled, zoom centers on map center")] public bool ZoomAtCursor = true; } + [Tooltip("Initial map center in latitude/longitude. Example: 40.7128, -74.0060 for New York")] + public LatitudeLongitude CenterLatitudeLongitude; + [Tooltip("Camera tilt angle. 90 = top-down, 15 = near-horizon")] [Range(15, 90)] public float Pitch; + [Tooltip("Camera rotation in degrees. 0 = north up")] [Range(-180, 180)] public float Bearing; - public float CameraDistance; + [Tooltip("Distance from ground target to camera along its forward axis. Higher = further away. Typical range: 100–1000 depending on scale")] + public float CameraDistance = 200f; [NonSerialized] public float ZoomValue; [NonSerialized] public float ScaleValue; private float _initialZoom; private float _initialScale; + [Tooltip("Enable left-click drag to pan the map")] public bool PanEnabled = true; - - // public bool RotateEnabled = true; - // public float RotationSpeed = 50.0f; + + [Tooltip("Camera rotation settings")] [SerializeField] private RotationSettings _rotationSettings; + [Tooltip("Zoom behaviour settings")] [SerializeField] private ZoomSettings _zoomSettings; private float _initialPitch; private Vector3 _previousScreenPosition; private Vector3 _dragOrigin; private Vector3 _targetPosition; - private Plane _controlPlane; - private float deltaAngleH; - private float deltaAngleV; - - public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane = null) + public override void Initialize(Camera camera, IMapInformation start) { - _camera = camera; - _controlPlane = controlPlane ?? new Plane(Vector3.up, Vector3.zero); + Initialize(camera, start, null); + } + + public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane) + { + base.Initialize(camera, start); + if (controlPlane.HasValue) _controlPlane = controlPlane.Value; if (_rotationSettings.MapRoot == null) { - _rotationSettings.MapRoot = GameObject.FindObjectOfType().transform; + var foundRoot = GameObject.FindObjectOfType(); + if (foundRoot == null) + { + Debug.LogError("SlippyMapCamera.Initialize: rotationSettings.MapRoot is unset and no MapBehaviourCore could be found in the scene. Camera rotation will not work; assign a MapRoot explicitly."); + return; + } + _rotationSettings.MapRoot = foundRoot.transform; } Pitch = start.Pitch; Bearing = start.Bearing; @@ -77,78 +97,129 @@ public void Initialize(Camera camera, IMapInformation start, Plane? controlPlane SetCamPositionByMapInfo(); start.SetView += SetCamera; } + + public override void Teardown(IMapInformation mapInfo) + { + // Detach the SetView subscription wired in Initialize. IMapInformation can + // outlive this camera (DontDestroyOnLoad / scene reload), so without the + // detach the closure roots us and _camera Transform indefinitely. + if (mapInfo != null) mapInfo.SetView -= SetCamera; + } - public override bool UpdateCamera(IMapInformation mapInformation) + private const float MaxMercatorLatitude = 85.06f; + + public override CameraOutput UpdateCamera(IMapInformation mapInformation) { - var hasChanged = false; + _output.Reset(); + UpdateInputState(); + + if (IsPointerOverUI()) + return _output; + + var pointerPos = GetPointerPosition(); Vector3 cursorHit; Vector2d newMercatorCenter = mapInformation.CenterMercator; - - if (!GetPlaneIntersection(Input.mousePosition, out cursorHit)) - return false; - - if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) + + if (!GetPlaneIntersection(pointerPos, out cursorHit)) + return _output; + + if (GetPointerDown() || GetSecondaryDown() || TouchCountDecreasedThisFrame) { - _previousScreenPosition = Input.mousePosition; + _previousScreenPosition = pointerPos; _dragOrigin = cursorHit; } - - if (Input.GetMouseButton(0) && PanEnabled) - { - Vector3 pos = cursorHit - _dragOrigin; + if (GetPointerHeld() && PanEnabled) + { var oldCursorLatLng = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(_dragOrigin))); var newCursorLatLng = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit))); - newMercatorCenter = mapInformation.CenterMercator - (newCursorLatLng - oldCursorLatLng); - hasChanged = true; + var mercatorDelta = newCursorLatLng - oldCursorLatLng; + // Skip the redraw when the cursor hit didn't actually move (Stationary + // touch, mouse-held-still). Avoids per-frame ChangeView round-trips. + if (mercatorDelta.x != 0 || mercatorDelta.y != 0) + { + newMercatorCenter = mapInformation.CenterMercator - mercatorDelta; + _output.HasChanged = true; + } } - else if (Input.GetMouseButton(1) && _rotationSettings.Enabled) + else if (GetSecondaryHeld() && _rotationSettings.Enabled) { - var deltaMousePos = (Input.mousePosition - _previousScreenPosition); - deltaAngleH = deltaMousePos.x; - deltaAngleV = deltaMousePos.y; + var deltaMousePos = (pointerPos - _previousScreenPosition); + var deltaAngleH = deltaMousePos.x; + var deltaAngleV = deltaMousePos.y; if (deltaAngleH != 0 || deltaAngleV != 0) { var currentPitch = deltaAngleV * Time.deltaTime * _rotationSettings.Speed; Pitch -= currentPitch; - Pitch = Mathf.Min(90, Mathf.Max(15, Pitch)); + Pitch = ClampPitch(Pitch); var currentBearing = deltaAngleH * Time.deltaTime * _rotationSettings.Speed; - Bearing += currentBearing; + Bearing = ClampBearing(Bearing + currentBearing); + _output.HasChanged = true; } } - else if (Input.mouseScrollDelta.magnitude > 0 && _zoomSettings.Enabled) + else if (GetPinchZoomDelta(out var zoomDelta) && _zoomSettings.Enabled) { + var zoomCenter = GetZoomCenter(); if (!_zoomSettings.ZoomAtCursor) { - var postZoom = ZoomValue + Input.GetAxis("Mouse ScrollWheel") * _zoomSettings.Speed; - ZoomValue = postZoom; + ZoomValue = ClampZoom(ZoomValue + zoomDelta * _zoomSettings.Speed); ScaleValue = _initialScale / Mathf.Pow(2, (ZoomValue - _initialZoom)); - hasChanged = true; + _output.HasChanged = true; } - else + else if (GetPlaneIntersection(zoomCenter, out var zoomHit)) { - var mouseMeter = cursorHit * ScaleValue; - var startingMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit))); - ZoomValue = ZoomValue + Input.GetAxis("Mouse ScrollWheel") * _zoomSettings.Speed; + var startingMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLng(_rotationSettings.MapRoot.InverseTransformPoint(zoomHit))); + ZoomValue = ClampZoom(ZoomValue + zoomDelta * _zoomSettings.Speed); ScaleValue = _initialScale / Mathf.Pow(2, (ZoomValue - _initialZoom)); - var newMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLngForScale(_rotationSettings.MapRoot.InverseTransformPoint(cursorHit), ScaleValue)); + var newMercatorCursor = Conversions.LatitudeLongitudeToWebMercator(mapInformation.ConvertPositionToLatLngForScale(_rotationSettings.MapRoot.InverseTransformPoint(zoomHit), ScaleValue)); var change = newMercatorCursor - startingMercatorCursor; newMercatorCenter = mapInformation.CenterMercator - change; - hasChanged = true; + _output.HasChanged = true; } } - - mapInformation.SetInformation(Conversions.WebMercatorToLatLon(newMercatorCenter), null, Pitch, Bearing); + + if (GetTwoFingerTiltDelta(out var tiltDelta)) + { + Pitch -= tiltDelta * _rotationSettings.Speed; + Pitch = ClampPitch(Pitch); + _output.HasChanged = true; + } + _dragOrigin = cursorHit; - _previousScreenPosition = Input.mousePosition; + _previousScreenPosition = pointerPos; - return hasChanged; + if (_output.HasChanged) + { + var latLng = Conversions.WebMercatorToLatLon(newMercatorCenter); + CenterLatitudeLongitude = new LatitudeLongitude( + System.Math.Clamp(latLng.Latitude, -(double)MaxMercatorLatitude, MaxMercatorLatitude), + latLng.Longitude); + SetCamPositionByMapInfo(); + _output.Center = CenterLatitudeLongitude; + _output.Zoom = ZoomValue; + _output.Pitch = Pitch; + _output.Bearing = Bearing; + _output.Scale = ScaleValue; + } + + return _output; } private void SetCamPositionByMapInfo() { _camera.transform.position = _targetPosition; - _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); + + if (_rotationSettings.rotationMode == RotationMode.RotateTheCamera) + { + _camera.transform.rotation = Quaternion.Euler(Pitch, Bearing, 0); + } + else if (_rotationSettings.rotationMode == RotationMode.RotateTheMap) + { + var projectedForward = Vector3.ProjectOnPlane(_camera.transform.forward, _controlPlane.normal); + var verticalRotation = Quaternion.AngleAxis(Pitch-_initialPitch, projectedForward.Perpendicular()); + _rotationSettings.MapRoot.rotation = verticalRotation * Quaternion.Euler(0, Bearing, 0); + } + _camera.transform.position += _camera.transform.forward * (-1f * CameraDistance); } @@ -157,8 +228,7 @@ public void SetCamera(IMapInformation mapInfo) _targetPosition = Vector3.zero; Pitch = mapInfo.Pitch; Bearing = mapInfo.Bearing; - //SetCamPositionByMapInfo(mapInfo); - + if (_rotationSettings.rotationMode == RotationMode.RotateTheCamera) { SetCamPositionByMapInfo(); @@ -167,33 +237,9 @@ public void SetCamera(IMapInformation mapInfo) { var projectedForward = Vector3.ProjectOnPlane(_camera.transform.forward, _controlPlane.normal); var verticalRotation = Quaternion.AngleAxis(Pitch-_initialPitch, projectedForward.Perpendicular()); - var vector = Quaternion.Euler(Pitch, Bearing, 0); - var worldVector = _camera.transform.rotation * vector; _rotationSettings.MapRoot.rotation = verticalRotation * Quaternion.Euler(0, Bearing, 0); } } - private bool GetPlaneIntersection(Vector3 screenPosition, out Vector3 hit) - { - hit = Vector3.zero; - var ray = _camera.ScreenPointToRay(screenPosition); - if (_controlPlane.Raycast(ray, out var distance)) - { - hit = ray.GetPoint(distance); - return true; - } - - return false; - } - - public Plane[] GetFrustrumPlanes() - { - return GeometryUtility.CalculateFrustumPlanes(_camera); - } - - public Transform GetTransform() - { - return _camera.transform; - } } } \ No newline at end of file diff --git a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs index afa88f5b1..23f5a53bc 100644 --- a/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs +++ b/Runtime/Mapbox/Example/Scripts/SlippyMapCameraBehaviour.cs @@ -1,36 +1,32 @@ using Mapbox.BaseModule.Map; -using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; using UnityEngine; +using UnityEngine.Serialization; namespace Mapbox.Example.Scripts.MapInput { - public class SlippyMapCameraBehaviour : MonoBehaviour + public class SlippyMapCameraBehaviour : MapCameraBehaviour { - public MapBehaviourCore MapBehaviour; - public Camera Camera; - public SlippyMapCamera Core; + [Tooltip("Slippy map camera settings. Camera stays static while the map moves underneath")] + [FormerlySerializedAs("Core")] + [SerializeField] private SlippyMapCamera _core; - private MapboxMap _map; - private bool _isInitialized = false; - - private void Awake() - { - MapBehaviour.Initialized += (map) => - { - _map = map; - _isInitialized = true; - Core.Initialize(Camera, _map.MapInformation, new Plane(MapBehaviour.transform.up, MapBehaviour.transform.position)); - }; - } + public override SlippyMapCamera Core => _core; - public void Update() + protected override void OnMapInitialized(MapboxMap map) { - if (_isInitialized && _map.MapInformation != null && Core.UpdateCamera(_map.MapInformation)) + // Defensive: if MapBehaviour.Initialized fires twice (re-init, swapped map + // asset), unsubscribe from the previous MapInformation before binding to + // the new one. Mirrors base.OnMapInitialized, but we can't just call base — + // it would invoke the 2-arg Core.Initialize and we'd re-init with the + // 3-arg control-plane overload below. + if (IsInitialized && Map != null && Map.MapInformation != null) { - var eulerAngles = Camera.transform.eulerAngles; - _map.MapInformation.SetInformation(null, Core.ZoomValue, eulerAngles.x, eulerAngles.y, Core.ScaleValue); + Core?.Teardown(Map.MapInformation); } + + Map = map; + IsInitialized = true; + _core.Initialize(Camera, map.MapInformation, new Plane(MapBehaviour.transform.up, MapBehaviour.transform.position)); } } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity b/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity index cc0b5a929..ba85225e5 100644 --- a/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity +++ b/Runtime/Mapbox/Example/Scripts/Test/ApiTest.unity @@ -658,6 +658,7 @@ MonoBehaviour: CoroutineStarter: {fileID: 1316629399} Root: {fileID: 1316629400} TileMaterials: [] + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} InitializeOnStart: 1 --- !u!4 &1316629400 Transform: diff --git a/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs b/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs index e40c13b62..f760d3731 100644 --- a/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs +++ b/Runtime/Mapbox/Example/Scripts/Test/FilterTest.cs @@ -1,10 +1,7 @@ -using System.Linq; using Mapbox.BaseModule.Utilities; using Mapbox.Example.Scripts.Map; -using Mapbox.Example.Scripts.ModuleBehaviours; using Mapbox.VectorModule; using Mapbox.VectorModule.Filters; -using Mapbox.VectorModule.Unity; using UnityEngine; namespace Mapbox.Example.Scripts.Test diff --git a/Runtime/Mapbox/GeocodingApi/Example.meta b/Runtime/Mapbox/GeocodingApi/Example.meta new file mode 100644 index 000000000..2d6725b9f --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 86a1539c780c646b1a14b57f16d0730d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs new file mode 100644 index 000000000..c9ef2f212 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs @@ -0,0 +1,157 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using UnityEngine; + +namespace Mapbox.GeocodingApi.Example +{ + /// + /// Example script demonstrating forward geocoding - converting place names to coordinates. + /// Forward geocoding takes a text query (address, place name, etc.) and returns geographic coordinates. + /// + public class ForwardGeocodeExample : MonoBehaviour + { + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Search Query")] + [Tooltip("The place name or address to search for")] + public string SearchQuery = "San Francisco, CA"; + + [Header("Optional Filters")] + [Tooltip("Limit results to specific countries (ISO 3166-1 alpha-2 codes, e.g., 'us', 'gb', 'de')")] + public string[] CountryFilter; + + [Tooltip("Enable autocomplete for partial queries")] + public bool EnableAutocomplete = false; + + [Tooltip("Bias results near this location (latitude)")] + public double ProximityLatitude = 0; + + [Tooltip("Bias results near this location (longitude)")] + public double ProximityLongitude = 0; + + [Tooltip("Use proximity bias")] + public bool UseProximity = false; + + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + Debug.Log("Geocoding API initialized. Searching for: " + SearchQuery); + SearchPlace(); + } + + /// + /// Perform a forward geocoding search with the configured query and filters. + /// + public void SearchPlace() + { + if (_geocodingApi == null) + { + Debug.LogWarning("Geocoding API not initialized yet. Waiting for map..."); + return; + } + + if (string.IsNullOrEmpty(SearchQuery)) + { + Debug.LogWarning("Search query is empty!"); + return; + } + + // Create forward geocode request + var forwardGeocodeResource = new ForwardGeocodeResource(SearchQuery); + + // Apply optional filters + if (CountryFilter != null && CountryFilter.Length > 0) + { + forwardGeocodeResource.Country = CountryFilter; + } + + if (EnableAutocomplete) + { + forwardGeocodeResource.Autocomplete = true; + } + + if (UseProximity) + { + forwardGeocodeResource.Proximity = new Vector2d(ProximityLatitude, ProximityLongitude); + } + + // Execute the geocoding request + _geocodingApi.Geocode(forwardGeocodeResource, HandleForwardGeocodeResponse); + } + + void HandleForwardGeocodeResponse(ForwardGeocodeResponse response) + { + if (response == null) + { + Debug.LogError("Geocoding API returned null response"); + return; + } + + if (response.Features == null || response.Features.Count == 0) + { + Debug.LogWarning($"No results found for query: {SearchQuery}"); + return; + } + + Debug.Log($"=== Found {response.Features.Count} Result(s) ==="); + + // Display all results (typically ordered by relevance) + for (int i = 0; i < response.Features.Count; i++) + { + var feature = response.Features[i]; + Debug.Log($"\n--- Result {i + 1} ---"); + Debug.Log($"Place Name: {feature.PlaceName}"); + Debug.Log($"Text: {feature.Text}"); + Debug.Log($"Coordinates: Lat {feature.Center.x}, Lng {feature.Center.y}"); + Debug.Log($"Place Type: {string.Join(", ", feature.PlaceType)}"); + Debug.Log($"Relevance: {feature.Relevance:F2}"); + + if (!string.IsNullOrEmpty(feature.Address)) + { + Debug.Log($"Address: {feature.Address}"); + } + + // Display context (city, state, country, etc.) + if (feature.Context != null && feature.Context.Count > 0) + { + Debug.Log("Context:"); + foreach (var context in feature.Context) + { + foreach (var kvp in context) + { + Debug.Log($" {kvp.Key}: {kvp.Value}"); + } + } + } + } + + // Highlight the best match + var bestMatch = response.Features[0]; + Debug.Log($"\n=== Best Match ==="); + Debug.Log($"{bestMatch.PlaceName}"); + Debug.Log($"Location: ({bestMatch.Center.x}, {bestMatch.Center.y})"); + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + } +} diff --git a/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta new file mode 100644 index 000000000..feff99ad1 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ForwardGeocodeExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56c2b9ba9277d45269cb7dd82054fa8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs new file mode 100644 index 000000000..f70d46b53 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs @@ -0,0 +1,172 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using UnityEngine; +using Mapbox.GeocodingApi; + +/// +/// Example script demonstrating reverse geocoding - converting coordinates to place names. +/// Reverse geocoding takes latitude/longitude coordinates and returns human-readable addresses. +/// +public class ReverseGeocodeExample : MonoBehaviour +{ + [Header("Map Reference")] + public MapBehaviourCore MapCore; + + [Header("Coordinates to Reverse Geocode")] + [Tooltip("Latitude of the location to look up (default: Golden Gate Bridge)")] + public double Latitude = 37.8199; + + [Tooltip("Longitude of the location to look up (default: Golden Gate Bridge)")] + public double Longitude = -122.4783; + + [Header("Optional Filters")] + [Tooltip("Filter by place type (e.g., 'address', 'poi', 'place', 'neighborhood')")] + public string[] TypeFilter; + + private MapboxGeocodingApi _geocodingApi; + + void Start() + { + if (MapCore == null) + { + Debug.LogError("MapCore is not assigned! Please assign a MapBehaviourCore component."); + return; + } + + MapCore.Initialized += OnMapInitialized; + } + + void OnMapInitialized(Mapbox.BaseModule.Map.MapboxMap map) + { + _geocodingApi = new MapboxGeocodingApi(map.MapService.FileSource); + Debug.Log($"Geocoding API initialized. Looking up coordinates: ({Latitude}, {Longitude})"); + GetAddressFromCoordinates(); + } + + /// + /// Perform a reverse geocoding lookup with the configured coordinates. + /// + public void GetAddressFromCoordinates() + { + if (_geocodingApi == null) + { + Debug.LogWarning("Geocoding API not initialized yet. Waiting for map..."); + return; + } + + // Create the location to reverse geocode + var location = new LatitudeLongitude(Latitude, Longitude); + + // Create reverse geocode request + var reverseGeocodeResource = new ReverseGeocodeResource(location); + + // Apply optional type filter + if (TypeFilter != null && TypeFilter.Length > 0) + { + reverseGeocodeResource.Types = TypeFilter; + } + + // Execute the reverse geocoding request + _geocodingApi.Geocode(reverseGeocodeResource, HandleReverseGeocodeResponse); + } + + void HandleReverseGeocodeResponse(ReverseGeocodeResponse response) + { + if (response == null) + { + Debug.LogError("Reverse geocoding API returned null response"); + return; + } + + if (response.Features == null || response.Features.Count == 0) + { + Debug.LogWarning($"No address found for coordinates: ({Latitude}, {Longitude})"); + return; + } + + Debug.Log($"=== Found {response.Features.Count} Feature(s) at Location ==="); + Debug.Log($"Query Coordinates: [{response.Query[0]}, {response.Query[1]}] (Lng, Lat)"); + + // Display all features at this location + for (int i = 0; i < response.Features.Count; i++) + { + var feature = response.Features[i]; + Debug.Log($"\n--- Feature {i + 1} ---"); + Debug.Log($"Place Name: {feature.PlaceName}"); + Debug.Log($"Text: {feature.Text}"); + Debug.Log($"Place Type: {string.Join(", ", feature.PlaceType)}"); + Debug.Log($"Relevance: {feature.Relevance:F2}"); + + if (!string.IsNullOrEmpty(feature.Address)) + { + Debug.Log($"Street Address: {feature.Address}"); + } + + // Display hierarchical context (neighborhood, city, state, country, etc.) + if (feature.Context != null && feature.Context.Count > 0) + { + Debug.Log("Location Context:"); + foreach (var context in feature.Context) + { + foreach (var kvp in context) + { + // Common keys: 'text' (name), 'short_code' (code) + if (kvp.Key == "text") + { + Debug.Log($" {kvp.Value}"); + } + } + } + } + } + + // Highlight the most specific result (usually first) + var primaryFeature = response.Features[0]; + Debug.Log($"\n=== Primary Address ==="); + Debug.Log(primaryFeature.PlaceName); + + // Build a simplified address string + string simpleAddress = primaryFeature.Text; + if (primaryFeature.Context != null) + { + foreach (var context in primaryFeature.Context) + { + if (context.ContainsKey("text")) + { + simpleAddress += ", " + context["text"]; + } + } + } + Debug.Log($"Simple Address: {simpleAddress}"); + } + + void OnDestroy() + { + if (MapCore != null) + { + MapCore.Initialized -= OnMapInitialized; + } + } + + // Optional: Helper method to reverse geocode from a Unity world position + // Useful for getting addresses from map clicks + public void GetAddressFromWorldPosition(Vector3 worldPosition) + { + if (MapCore == null || MapCore.InitializationStatus <= InitializationStatus.Initialized) + { + Debug.LogWarning("Map not initialized yet"); + return; + } + + // Convert world position to lat/lng + var latLng = MapCore.MapInformation.ConvertPositionToLatLng(worldPosition); + + // Update the inspector values + Latitude = latLng.Latitude; + Longitude = latLng.Longitude; + + // Perform reverse geocoding + GetAddressFromCoordinates(); + } +} diff --git a/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta new file mode 100644 index 000000000..c595e79d8 --- /dev/null +++ b/Runtime/Mapbox/GeocodingApi/Example/ReverseGeocodeExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8fabaff234de48a0b6ec6cb3c052866 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/GeocodingApi/Response/Feature.cs b/Runtime/Mapbox/GeocodingApi/Response/Feature.cs index b8780ba04..023e2a2c9 100644 --- a/Runtime/Mapbox/GeocodingApi/Response/Feature.cs +++ b/Runtime/Mapbox/GeocodingApi/Response/Feature.cs @@ -26,7 +26,14 @@ public class Feature { /// The type. [JsonProperty("type")] public string Type; - + + /// + /// Gets or sets the types of the place. + /// + /// The types of the place. + [JsonProperty("place_type")] + public List PlaceType{ get; set; } + /// /// Gets or sets the text. /// diff --git a/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity b/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity index 5166e51cd..755b88176 100644 --- a/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity +++ b/Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo/GeocodingDemo.unity @@ -1449,7 +1449,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 1316629400} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629398} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 2054936219} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Runtime/Mapbox/ImageModule/Editor.meta b/Runtime/Mapbox/ImageModule/Editor.meta new file mode 100644 index 000000000..3638540cb --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee3afe987f07c4883bfcd9948b4718c4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef new file mode 100644 index 000000000..d83cacdf6 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "MapboxImageryModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:5ff00896142b94bc7a58a7c72b2faf57", + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta new file mode 100644 index 000000000..87513c2b4 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/MapboxImageryModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15fe16679907f4627ab38b784df8ca99 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs new file mode 100644 index 000000000..4f2a1da46 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs @@ -0,0 +1,152 @@ +using System.Linq; +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for fields tagged with . + /// Renders a dropdown of distinct-grid presets (each halving the resolution) and a + /// live HelpBox describing the resulting grid, upgrading to a warning at coarse values. + /// Legacy non-preset serialized values are snapped to the nearest preset. + /// + [CustomPropertyDrawer(typeof(SimplificationFactorAttribute))] + public class SimplificationFactorDrawer : PropertyDrawer + { + private const float Spacing = 2f; + + // Only factors that produce a distinct grid size are worth exposing — everything in + // between collapses via integer division. Each entry halves the grid resolution. + private static readonly int[] PresetFactors = { 1, 2, 4, 8, 16, 32, 64, 128 }; + + // Cached preset+label arrays per attribute. OnGUI runs every repaint; without this + // the LINQ chain allocates fresh arrays continuously while the Inspector is open. + private int[] _cachedFactors; + private GUIContent[] _cachedLabels; + private int _cachedMin = -1; + private int _cachedMax = -1; + private int _cachedVertexBase = -1; + + private void EnsurePresetsCached(SimplificationFactorAttribute attr) + { + if (_cachedFactors != null && _cachedMin == attr.Min && _cachedMax == attr.Max && _cachedVertexBase == attr.VertexBase) + { + return; + } + _cachedFactors = PresetFactors.Where(f => f >= attr.Min && f <= attr.Max).ToArray(); + _cachedLabels = _cachedFactors.Select(f => new GUIContent(BuildLabel(f, attr.VertexBase))).ToArray(); + _cachedMin = attr.Min; + _cachedMax = attr.Max; + _cachedVertexBase = attr.VertexBase; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + label = EditorGUI.BeginProperty(position, label, property); + + var attr = (SimplificationFactorAttribute)attribute; + EnsurePresetsCached(attr); + var factors = _cachedFactors; + var labels = _cachedLabels; + + var current = property.intValue; + var inPresets = System.Array.IndexOf(factors, current) >= 0; + // Don't write through SerializedProperty during paint just because the + // value isn't in the preset list — that silently dirties every legacy + // asset on first open. Show a warning and let the user pick a preset + // (which Unity then records as a deliberate, undoable change). + var fieldRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + EditorGUI.BeginChangeCheck(); + EditorGUI.showMixedValue = property.hasMultipleDifferentValues; + var newValue = EditorGUI.IntPopup(fieldRect, label, current, labels, factors); + EditorGUI.showMixedValue = false; + if (EditorGUI.EndChangeCheck()) + { + property.intValue = newValue; + } + + string message; + MessageType severity; + if (!inPresets) + { + message = $"Legacy value {current} is not a preset. Pick a preset above to migrate; the value is otherwise left untouched."; + severity = MessageType.Warning; + } + else + { + (message, severity) = BuildMessage(property.intValue, attr.VertexBase); + } + var helpHeight = CalcHelpBoxHeight(message, position.width); + var helpRect = new Rect(position.x, fieldRect.yMax + Spacing, position.width, helpHeight); + EditorGUI.HelpBox(helpRect, message, severity); + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var attr = (SimplificationFactorAttribute)attribute; + EnsurePresetsCached(attr); + // Mirror OnGUI's branch so the help-box height matches the actual rendered + // message. The legacy-value warning is longer than the preset description, + // and using the preset string for height while painting the legacy string + // caused the HelpBox to clip and overlap the next inspector row. + var current = property.intValue; + var inPresets = System.Array.IndexOf(_cachedFactors, current) >= 0; + string message; + if (!inPresets) + { + message = $"Legacy value {current} is not a preset. Pick a preset above to migrate; the value is otherwise left untouched."; + } + else + { + (message, _) = BuildMessage(current, attr.VertexBase); + } + return EditorGUIUtility.singleLineHeight + Spacing + CalcHelpBoxHeight(message, EditorGUIUtility.currentViewWidth - 40f); + } + + private static string BuildLabel(int factor, int vertexBase) + { + var sampleCount = vertexBase / Mathf.Max(1, factor); + var gridSide = sampleCount + 1; + return $"{factor} ({gridSide}x{gridSide} verts)"; + } + + private static (string message, MessageType severity) BuildMessage(int factor, int vertexBase) + { + var clampedFactor = Mathf.Max(1, factor); + var sampleCount = vertexBase / clampedFactor; + var gridSide = sampleCount + 1; + var verts = gridSide * gridSide; + var baseLine = $"{gridSide}x{gridSide} vertex grid per tile ({verts} verts, {sampleCount}x{sampleCount} quads)."; + + if (sampleCount >= 64) + { + return (baseLine + " Highest visual quality — heaviest CPU/GPU cost, especially with colliders enabled.", MessageType.Info); + } + if (sampleCount >= 16) + { + return (baseLine + " Good balance of detail and performance.", MessageType.Info); + } + if (sampleCount >= 8) + { + return (baseLine + " Lower-density geometry — fine terrain details will be lost.", MessageType.Info); + } + return (baseLine + " Very coarse — terrain will look blocky and neighboring tiles may visibly mismatch at seams.", MessageType.Warning); + } + + // Reused across CalcHelpBoxHeight calls. GetPropertyHeight runs multiple times + // per repaint while the inspector is open; allocating a fresh GUIContent each + // call shows up as steady GC churn even in idle Editor sessions. + private static readonly GUIContent _helpBoxContent = new GUIContent(); + + private static float CalcHelpBoxHeight(string message, float width) + { + _helpBoxContent.text = message; + var style = EditorStyles.helpBox; + var textWidth = Mathf.Max(40f, width - 32f); + return Mathf.Max(EditorGUIUtility.singleLineHeight * 2f, style.CalcHeight(_helpBoxContent, textWidth)); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta new file mode 100644 index 000000000..02728c72f --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/SimplificationFactorDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 75571eeda31dd488295a98b2985edfd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs new file mode 100644 index 000000000..bc0e02989 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs @@ -0,0 +1,75 @@ +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for . Grays out the + /// dedicated-layer options until addCollider is enabled, and further grays out + /// the layer dropdown until useDedicatedColliderLayer is enabled. Same reveal + /// pattern used by and + /// . + /// + [CustomPropertyDrawer(typeof(TerrainColliderOptions))] + public class TerrainColliderOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var foldoutRect = new Rect(position.x, position.y, position.width, line); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (property.isExpanded) + { + var addCollider = property.FindPropertyRelative("addCollider"); + var asyncBake = property.FindPropertyRelative("asyncBakeCollider"); + var useDedicated = property.FindPropertyRelative("useDedicatedColliderLayer"); + var colliderLayer = property.FindPropertyRelative("colliderLayerId"); + + EditorGUI.indentLevel++; + var y = position.y + line + spacing; + + EditorGUI.PropertyField(new Rect(position.x, y, position.width, line), addCollider); + y += line + spacing; + + using (new EditorGUI.DisabledScope(!addCollider.boolValue)) + { + EditorGUI.PropertyField(new Rect(position.x, y, position.width, line), asyncBake); + y += line + spacing; + + EditorGUI.PropertyField(new Rect(position.x, y, position.width, line), useDedicated); + y += line + spacing; + + using (new EditorGUI.DisabledScope(!useDedicated.boolValue)) + { + var layerHeight = EditorGUI.GetPropertyHeight(colliderLayer, true); + EditorGUI.PropertyField(new Rect(position.x, y, position.width, layerHeight), colliderLayer); + } + } + EditorGUI.indentLevel--; + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + if (!property.isExpanded) + { + return line; + } + + var colliderLayer = property.FindPropertyRelative("colliderLayerId"); + // foldout + addCollider + asyncBake + useDedicated + colliderLayer = 4 line-rows + 1 var-row + return line + spacing + line + spacing + line + spacing + line + spacing + EditorGUI.GetPropertyHeight(colliderLayer, true); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta new file mode 100644 index 000000000..2c414051a --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainColliderOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c02662f531ec9466abc6d9891a8f7120 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs new file mode 100644 index 000000000..2af15bcb0 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs @@ -0,0 +1,38 @@ +using Mapbox.Example.Scripts.ModuleBehaviours; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Custom inspector for TerrainLayerModuleScript. Delegates settings rendering + /// to so the extract-on-override pattern + /// and scene-aware warnings are consistent between terrain variants. + /// + [CustomEditor(typeof(TerrainLayerModuleScript))] + public class TerrainLayerModuleScriptEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Standard script field. + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script")); + } + + // Draw everything except Settings with the default renderer. + DrawPropertiesExcluding(serializedObject, "m_Script", "Settings"); + + // Custom rendering for Settings so we can intercept ExtractCpuElevationData. + var settingsProp = serializedObject.FindProperty("Settings"); + if (settingsProp != null) + { + TerrainSettingsInspectorHelper.Draw(settingsProp); + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta new file mode 100644 index 000000000..b235c9e0f --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainLayerModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53d6fa2e8e0194e65a0de0268dc5bfbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs new file mode 100644 index 000000000..e78030159 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs @@ -0,0 +1,162 @@ +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Renders a TerrainLayerModuleSettings SerializedProperty with two inspector + /// enhancements: (1) the ExtractCpuElevationData toggle is force-checked and + /// disabled when another setting requires CPU elevation; (2) a warning HelpBox appears + /// when the scene contains a module that typically needs CPU elevation but extraction + /// is user-disabled. Shared by both terrain-settings editors. + /// + public static class TerrainSettingsInspectorHelper + { + private const string ExtractFieldName = "ExtractCpuElevationData"; + private const string UseShaderFieldName = "UseShaderTerrain"; + private const string ElevationPropsFieldName = "ElevationLayerProperties"; + private const string ColliderOptionsFieldName = "colliderOptions"; + private const string AddColliderFieldName = "addCollider"; + + public static void Draw(SerializedProperty settingsProp) + { + settingsProp.isExpanded = EditorGUILayout.Foldout(settingsProp.isExpanded, settingsProp.displayName, true); + if (!settingsProp.isExpanded) + { + return; + } + + var useShaderProp = settingsProp.FindPropertyRelative(UseShaderFieldName); + var colliderProp = settingsProp + .FindPropertyRelative(ElevationPropsFieldName) + ?.FindPropertyRelative(ColliderOptionsFieldName) + ?.FindPropertyRelative(AddColliderFieldName); + + var useShaderOff = useShaderProp != null && !useShaderProp.boolValue; + var colliderOn = colliderProp != null && colliderProp.boolValue; + var forcedOn = useShaderOff || colliderOn; + var reason = useShaderOff + ? "CPU-elevation rendering (UseShaderTerrain is off)" + : "the terrain collider (addCollider is on)"; + + EditorGUI.indentLevel++; + var child = settingsProp.Copy(); + var end = settingsProp.GetEndProperty(); + child.NextVisible(true); + while (!SerializedProperty.EqualContents(child, end)) + { + if (child.name == ExtractFieldName && forcedOn) + { + // Display-only override. We do NOT write child.boolValue = true here: + // the runtime evaluates `TerrainLayerModuleSettings.NeedsCpuElevation` + // (an OR over the forcing flags) directly, so the persisted value + // stays as the user set it. Writing during OnGUI would silently dirty + // every asset/scene on inspection. + using (new EditorGUI.DisabledScope(true)) + { + EditorGUI.showMixedValue = false; + EditorGUILayout.Toggle(new GUIContent(child.displayName, child.tooltip), true); + EditorGUI.showMixedValue = child.hasMultipleDifferentValues; + } + EditorGUILayout.HelpBox( + $"Extraction is force-enabled by {reason}. CPU elevation will be decoded regardless of this checkbox.", + MessageType.Info); + } + else + { + EditorGUILayout.PropertyField(child, true); + } + + if (!child.NextVisible(false)) + { + break; + } + } + EditorGUI.indentLevel--; + + DrawSceneNeedsExtractionWarning(settingsProp); + } + + // Warns if extraction is genuinely user-disabled (box off AND nothing forces it on) + // while the scene contains modules that typically need ground heights. + private static void DrawSceneNeedsExtractionWarning(SerializedProperty settingsProp) + { + var extractProp = settingsProp.FindPropertyRelative(ExtractFieldName); + if (extractProp == null || extractProp.boolValue) + { + return; + } + var useShaderProp = settingsProp.FindPropertyRelative(UseShaderFieldName); + var colliderProp = settingsProp + .FindPropertyRelative(ElevationPropsFieldName) + ?.FindPropertyRelative(ColliderOptionsFieldName) + ?.FindPropertyRelative(AddColliderFieldName); + var forcedOn = + (useShaderProp != null && !useShaderProp.boolValue) || + (colliderProp != null && colliderProp.boolValue); + if (forcedOn) + { + return; + } + + if (SceneHasFeaturesNeedingCpuElevation(out var detected)) + { + EditorGUILayout.HelpBox( + $"CPU elevation extraction is disabled, but this scene contains a {detected} that typically snaps to terrain. " + + "Ground-clamped features will be placed at Y=0 and the elevation query APIs will return 0. " + + "Re-enable ExtractCpuElevationData unless you are sure nothing in this scene needs ground heights.", + MessageType.Warning); + } + } + + private static readonly string[] WatchedTypeNames = + { + "VectorLayerModuleScript", + "MapboxComponentsModuleScript" + }; + + // Cached scene-scan result, invalidated whenever the hierarchy changes. Without this, + // every Inspector OnGUI repaint runs FindObjectsByType across the + // whole active scene — visibly stalls when many tiles are alive. + private static bool _sceneScanValid; + private static bool _sceneScanResult; + private static string _sceneScanDetectedTypeName; + private static bool _hierarchyHookInstalled; + + private static bool SceneHasFeaturesNeedingCpuElevation(out string detectedTypeName) + { + if (!_hierarchyHookInstalled) + { + EditorApplication.hierarchyChanged += () => _sceneScanValid = false; + _hierarchyHookInstalled = true; + } + if (_sceneScanValid) + { + detectedTypeName = _sceneScanDetectedTypeName; + return _sceneScanResult; + } + + _sceneScanDetectedTypeName = null; + _sceneScanResult = false; + var all = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + foreach (var mb in all) + { + if (mb == null) continue; + var name = mb.GetType().Name; + for (int i = 0; i < WatchedTypeNames.Length; i++) + { + if (name == WatchedTypeNames[i]) + { + _sceneScanDetectedTypeName = name; + _sceneScanResult = true; + goto done; + } + } + } + done: + _sceneScanValid = true; + detectedTypeName = _sceneScanDetectedTypeName; + return _sceneScanResult; + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta new file mode 100644 index 000000000..65548fb81 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSettingsInspectorHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c5f5594527a944408ba2d33d4134cde +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs new file mode 100644 index 000000000..aa38463ef --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs @@ -0,0 +1,62 @@ +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for . Mirrors the + /// pattern: wallHeight is always visible + /// but grayed out until isActive is enabled. + /// + [CustomPropertyDrawer(typeof(TerrainSideWallOptions))] + public class TerrainSideWallOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var foldoutRect = new Rect(position.x, position.y, position.width, line); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (property.isExpanded) + { + var isActive = property.FindPropertyRelative("isActive"); + var wallHeight = property.FindPropertyRelative("wallHeight"); + + EditorGUI.indentLevel++; + var y = position.y + line + spacing; + + var activeRect = new Rect(position.x, y, position.width, line); + EditorGUI.PropertyField(activeRect, isActive); + y += line + spacing; + + var heightRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(wallHeight, true)); + using (new EditorGUI.DisabledScope(!isActive.boolValue)) + { + EditorGUI.PropertyField(heightRect, wallHeight); + } + EditorGUI.indentLevel--; + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + if (!property.isExpanded) + { + return line; + } + + var wallHeight = property.FindPropertyRelative("wallHeight"); + return line + spacing + line + spacing + EditorGUI.GetPropertyHeight(wallHeight, true); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta new file mode 100644 index 000000000..fe914cf83 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/TerrainSideWallOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 616c73c44390a4d9999d07a80b1c02ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs new file mode 100644 index 000000000..5d64df175 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs @@ -0,0 +1,64 @@ +using Mapbox.ImageModule.Terrain.Settings; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.ImageModule.Editor +{ + /// + /// Inspector drawer for . Shows addToLayer as a + /// checkbox and the layerId dropdown (via the shared GameObjectLayer + /// drawer) grayed-out when addToLayer is off, so users can see the selected + /// layer but can't change it until they opt in. + /// + [CustomPropertyDrawer(typeof(UnityLayerOptions))] + public class UnityLayerOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var foldoutRect = new Rect(position.x, position.y, position.width, line); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (property.isExpanded) + { + var addToLayer = property.FindPropertyRelative("addToLayer"); + var layerId = property.FindPropertyRelative("layerId"); + + EditorGUI.indentLevel++; + var y = position.y + line + spacing; + + var addRect = new Rect(position.x, y, position.width, line); + EditorGUI.PropertyField(addRect, addToLayer); + y += line + spacing; + + var layerRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(layerId, true)); + using (new EditorGUI.DisabledScope(!addToLayer.boolValue)) + { + EditorGUI.PropertyField(layerRect, layerId); + } + EditorGUI.indentLevel--; + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var line = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + if (!property.isExpanded) + { + return line; + } + + var layerId = property.FindPropertyRelative("layerId"); + // foldout + addToLayer + layerId (always rendered, just grayed out when inactive). + return line + spacing + line + spacing + EditorGUI.GetPropertyHeight(layerId, true); + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta new file mode 100644 index 000000000..cd3310a02 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Editor/UnityLayerOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55d9694a92c84468f9c28a997b2320bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef b/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef index 401a0f3c9..926ca1fdd 100644 --- a/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef +++ b/Runtime/Mapbox/ImageModule/MapboxImageModule.asmdef @@ -2,7 +2,8 @@ "name": "MapboxImageryModule", "rootNamespace": "", "references": [ - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:36ca6af6e2b304d4090888554d6ce199" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs b/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs index bce329f02..a09243938 100644 --- a/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/StaticApiLayerModule.cs @@ -15,6 +15,7 @@ public class StaticApiLayerModule : ILayerModule { protected StaticLayerModuleSettings _settings; protected Source _rasterSource; + public Source RasterSource => _rasterSource; private HashSet _retainedTiles; public StaticApiLayerModule(Source source, StaticLayerModuleSettings settings) : base() @@ -79,6 +80,11 @@ public void UpdatePositioning(IMapInformation mapInfo) { } + + public IEnumerator ChangeTilesetId(string tilesetId) + { + yield return _rasterSource.ChangeTilesetId(tilesetId); + } public virtual void OnDestroy() { diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs new file mode 100644 index 000000000..a2a826f6e --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using UnityEngine; +using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; + +namespace Mapbox.ImageModule.Terrain +{ + /// + /// Watches the visualizer's tile lifecycle events and keeps + /// Min/MaxElevation in sync with the + /// observed elevation range across loaded tiles. + /// + /// Owned by and constructed when the module + /// is attached to a visualizer. The visualizer itself doesn't know this exists. + /// + /// Three lifecycle hooks feed dirty-flag updates: + /// + /// TileLoaded: data may be ready (mark dirty) or shader-mode pending + /// CPU decode (subscribe a one-shot widening to ElevationValuesUpdated). + /// TileUnloading: if the unloading tile held a boundary value, or is + /// the last contributor, mark dirty so the next drain re-derives from scratch. + /// Per-frame drain: a coroutine recomputes from ActiveTiles when the + /// flag is set — one O(N) pass per frame regardless of event volume. + /// + /// + internal sealed class TerrainBoundsTracker : IDisposable + { + private readonly ITileLifecycleSource _source; + + // One-shot widening subscriptions for shader-mode tiles whose CPU decode + // hasn't arrived yet. Stored by (data, handler) so Dispose can detach the + // closures and so the dedup loop can avoid attaching twice for the same + // shared data tile. + private readonly List<(TerrainData data, Action handler)> _pendingWatches = + new List<(TerrainData, Action)>(); + + private bool _dirty; + private bool _disposed; + private Coroutine _drainCoroutine; + + public TerrainBoundsTracker(ITileLifecycleSource source) + { + _source = source; + _source.TileLoaded += OnTileLoaded; + _source.TileUnloading += OnTileUnloading; + if (Runnable.Instance != null) + { + _drainCoroutine = Runnable.Instance.StartCoroutine(DrainLoop()); + } + else + { + // Should never happen in normal play (Runnable is a global singleton). + // Fail loud so it's obvious if the construction order ever changes. + Debug.LogWarning("TerrainBoundsTracker: Runnable.Instance is null at construction; bounds recompute coroutine not started. Terrain.Min/MaxElevation will not update."); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_source != null) + { + _source.TileLoaded -= OnTileLoaded; + _source.TileUnloading -= OnTileUnloading; + } + + // Detach pending watches. Both closures dropped — the dispose callback + // keeps a Dictionary reference alive otherwise. + for (int i = 0; i < _pendingWatches.Count; i++) + { + var (data, handler) = _pendingWatches[i]; + if (data != null && handler != null) + { + data.ElevationValuesUpdated -= handler; + } + } + _pendingWatches.Clear(); + + if (_drainCoroutine != null && Runnable.Instance != null) + { + Runnable.Instance.StopCoroutine(_drainCoroutine); + _drainCoroutine = null; + } + } + + private IEnumerator DrainLoop() + { + while (!_disposed) + { + if (_dirty) + { + Recompute(); + _dirty = false; + } + yield return null; + } + } + + private void OnTileLoaded(UnityMapTile tile) + { + if (_disposed) return; + var terrainData = tile.TerrainContainer?.TerrainData; + if (terrainData == null) return; + + if (terrainData.IsElevationDataReady) + { + // Data already decoded — mark dirty for the next drain tick. + _dirty = true; + } + else + { + // Shader-mode tiles finish on texture-ready, before the CPU decode arrives. + // Hook a one-shot watch so the bounds widen when the decode lands. + WatchForAsyncDecode(terrainData); + } + } + + private void OnTileUnloading(UnityMapTile tile) + { + if (_disposed) return; + + // Snapshot the pooled tile's contribution to the current bounds. Tile.Recycle() + // (called by PoolTile after this event fires) clears TerrainData, so we have to + // check up front. Skip when both endpoints are 0 — the tile is flat and didn't + // actually contribute, so a recompute would just produce the same values. + var terrain = _source.MapInformation.Terrain; + var terrainData = tile.TerrainContainer?.TerrainData; + // Exact equality: terrain.MaxElevation was assigned directly from some tile's + // MaxElevation in the previous Recompute, so `==` matches when this tile is + // that contributor. Mathf.Approximately's magnitude-scaled tolerance would + // also accept near-matches from sibling tiles, triggering redundant recomputes. + bool contributedMax = terrainData != null && terrainData.IsElevationDataReady && + terrainData.MaxElevation != 0f && + terrainData.MaxElevation == terrain.MaxElevation; + bool contributedMin = terrainData != null && terrainData.IsElevationDataReady && + terrainData.MinElevation != 0f && + terrainData.MinElevation == terrain.MinElevation; + + // Also dirty when this is the last tile being pooled — an all-flat session + // (every loaded tile had Min=Max=0, or no terrain data at all) wouldn't trip + // the non-zero checks above, so the empty-fallback (reset to TerrainInfo + // defaults) would never run when the map empties out. ActiveTiles.Count == 1 + // here because TileUnloading fires before the visualizer's ActiveTiles.Remove. + // No terrainData requirement: shader-only tiles count too. + var activeTiles = _source.ActiveTiles; + bool wasLastActive = activeTiles.Count == 1 && + activeTiles.ContainsKey(tile.UnwrappedTileId); + + if (contributedMax || contributedMin || wasLastActive) + { + _dirty = true; + } + } + + private void WatchForAsyncDecode(TerrainData data) + { + // De-dup: shared TerrainData is referenced by up to 16 render tiles (data zoom + // = render zoom − 2). One watch per data is enough — the dirty flag fires once. + for (int i = 0; i < _pendingWatches.Count; i++) + { + if (ReferenceEquals(_pendingWatches[i].data, data)) return; + } + + Action handler = null; + Action onDispose = null; + handler = () => + { + data.ElevationValuesUpdated -= handler; + data.RemoveDisposeCallback(onDispose); + RemoveWatchEntry(data, handler); + if (!_disposed) _dirty = true; + }; + // TerrainData.Dispose doesn't fire ElevationValuesUpdated, so without this hook + // the watch entry would leak (rooting both data and the closure) until our own + // Dispose. Eviction during the decode window is the trigger. + onDispose = () => + { + data.ElevationValuesUpdated -= handler; + data.RemoveDisposeCallback(onDispose); + RemoveWatchEntry(data, handler); + }; + + _pendingWatches.Add((data, handler)); + data.ElevationValuesUpdated += handler; + data.AddDisposeCallback(onDispose); + } + + private void RemoveWatchEntry(TerrainData data, Action handler) + { + for (int i = _pendingWatches.Count - 1; i >= 0; i--) + { + if (ReferenceEquals(_pendingWatches[i].data, data) && + ReferenceEquals(_pendingWatches[i].handler, handler)) + { + _pendingWatches.RemoveAt(i); + break; + } + } + } + + private void Recompute() + { + var terrain = _source.MapInformation.Terrain; + float max = 0f; + float min = 0f; + bool any = false; + foreach (var kv in _source.ActiveTiles) + { + var td = kv.Value.TerrainContainer?.TerrainData; + if (td == null || !td.IsElevationDataReady) continue; + if (!any) + { + max = td.MaxElevation; + min = td.MinElevation; + any = true; + } + else + { + if (td.MaxElevation > max) max = td.MaxElevation; + if (td.MinElevation < min) min = td.MinElevation; + } + } + // When no tile is contributing, fall back to TerrainInfo's conservative defaults + // rather than 0 — a flat AABB would frustum-cull plausible mountains on the next + // frame before any sample arrives. + terrain.MaxElevation = any ? max : TerrainInfo.DefaultMaxElevation; + terrain.MinElevation = any ? min : TerrainInfo.DefaultMinElevation; + } + } +} diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta new file mode 100644 index 000000000..af75e7f86 --- /dev/null +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainBoundsTracker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf4c61eb471041da93a691fb5473f526 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs index 310f1f4c5..649d56532 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModule.cs @@ -5,20 +5,47 @@ using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; using Mapbox.ImageModule.Terrain.TerrainStrategies; +using Mapbox.UnityMapService.DataSources; using UnityEngine; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.ImageModule.Terrain { - public class TerrainLayerModule : ITerrainLayerModule + /// + /// Terrain layer module. Orchestrates loading of terrain-RGB raster tiles from a + /// , configures CPU elevation extraction based on + /// settings, and hands each tile through a that produces + /// render + optional collider meshes. + /// + public class TerrainLayerModule : ITerrainLayerModule, ITileLifecycleObserver { private TerrainLayerModuleSettings _settings; private Source _rasterSource; private HashSet _retainedTerrainTiles; private TerrainStrategy _terrainStrategy; + // Owns the Min/MaxElevation bookkeeping that used to live in MapboxMapVisualizer. + // Constructed in AttachToMapVisualizer once the visualizer is available, disposed + // in OnDestroy. The visualizer never references this directly. + private TerrainBoundsTracker _boundsTracker; + // One-shot guard so QueryElevation doesn't spam the console on every call when the + // user genuinely disabled CPU extraction. + private bool _elevationDisabledWarningLogged; + // Reused across GetDataId(IEnumerable) calls to avoid allocating a fresh HashSet + // plus iterator chain from Where/Select/Distinct on every tile-cover update. + private readonly HashSet _dataIdScratch = new HashSet(); + + public void AttachToMapVisualizer(ITileLifecycleSource source) + { + // Idempotent: if a tracker is already running (re-init scenarios), keep it. + // Constructing a second one would leak the first (coroutine + subscriptions). + if (_boundsTracker != null) return; + _boundsTracker = new TerrainBoundsTracker(source); + } public TerrainLayerModule(Source source, TerrainLayerModuleSettings settings) : base() { @@ -31,6 +58,17 @@ public TerrainLayerModule(Source source, TerrainLayerModuleSettings public virtual IEnumerator Initialize() { yield return _rasterSource.Initialize(); + if (_rasterSource is TerrainSource terrainSource) + { + terrainSource.ExtractCpuElevationData = _settings.NeedsCpuElevation; + } + if (!_settings.ExtractCpuElevationData && _settings.NeedsCpuElevation) + { + var reason = !_settings.UseShaderTerrain + ? "UseShaderTerrain is off (CPU-elevation rendering needs the data)" + : "colliderOptions.addCollider is on (terrain collider needs the data)"; + Debug.LogWarning($"[Mapbox] TerrainLayerModuleSettings.ExtractCpuElevationData is unchecked, but {reason}. Extraction has been force-enabled; the checkbox has no effect in this configuration."); + } _terrainStrategy.Initialize(_settings.ElevationLayerProperties); if(_settings.LoadBackgroundTextures) { @@ -45,13 +83,13 @@ public virtual void LoadTempTile(UnityMapTile unityTile) unityTile.TerrainContainer.DisableTerrain(); return; } - + var targetTileId = GetDataId(unityTile.CanonicalTileId); var parentTileId = targetTileId; for (int i = parentTileId.Z; i >= 2; i--) { parentTileId.MoveToParent(); - if (_rasterSource.GetInstantData(parentTileId, out var instantData) && instantData.IsElevationDataReady) + if (_rasterSource.GetInstantData(parentTileId, out var instantData) && IsTileReady(instantData)) { unityTile.TerrainContainer.SetTerrainData(instantData, _settings.UseShaderTerrain, TileContainerState.Temporary); _terrainStrategy.RegisterTile(unityTile, !_settings.UseShaderTerrain); @@ -59,7 +97,7 @@ public virtual void LoadTempTile(UnityMapTile unityTile) } } } - + public virtual bool LoadInstant(UnityMapTile unityTile) { if (IsZinSupportedRange(unityTile.CanonicalTileId.Z) == false) @@ -67,18 +105,29 @@ public virtual bool LoadInstant(UnityMapTile unityTile) unityTile.TerrainContainer.DisableTerrain(); return true; } - + var targetTileId = GetDataId(unityTile.CanonicalTileId); - if (_rasterSource.GetInstantData(targetTileId, out var instantData) && instantData.IsElevationDataReady) + if (_rasterSource.GetInstantData(targetTileId, out var instantData) && IsTileReady(instantData)) { - unityTile.TerrainContainer.SetTerrainData(instantData, _settings.UseShaderTerrain); + unityTile.TerrainContainer.SetTerrainData(instantData, _settings.UseShaderTerrain, TileContainerState.Final); _terrainStrategy.RegisterTile(unityTile, !_settings.UseShaderTerrain); return true; } - + return false; } + /// + /// Readiness gate used by and . + /// Shader-elevation rendering only needs the raster texture and can display a tile + /// as soon as the texture exists. CPU-elevation rendering still needs the per-pixel + /// float[] before displacing verts, so it must wait for extraction to finish. + /// + private bool IsTileReady(TerrainData data) + { + return _settings.UseShaderTerrain ? data.IsTextureReady : data.IsElevationDataReady; + } + public virtual bool RetainTiles(HashSet retainedTiles) { var isReady = true; @@ -91,22 +140,6 @@ public virtual bool RetainTiles(HashSet retainedTiles) isReady = _rasterSource.RetainTiles(_retainedTerrainTiles); return isReady; } - - public float QueryElevation(CanonicalTileId tileId, float x, float y) - { - var originalTileId = tileId; - var targetTileId = tileId; - for (int i = 0; i < 5; i++) - { - if (_rasterSource.GetInstantData(targetTileId, out var instantData)) - { - return instantData.QueryHeightData(originalTileId, x, y); - } - targetTileId.MoveToParent(); - } - - return 0; - } public void UpdatePositioning(IMapInformation mapInfo) { @@ -115,31 +148,42 @@ public void UpdatePositioning(IMapInformation mapInfo) public void OnDestroy() { + _boundsTracker?.Dispose(); + _boundsTracker = null; _rasterSource.OnDestroy(); + _terrainStrategy?.OnDestroy(); } //COROUTINE METHODS only used in initialization so far #region coroutine methods - public virtual IEnumerator LoadTileData(CanonicalTileId tileId, Action callback = null) + public virtual IEnumerator LoadTileData(CanonicalTileId tileId, Action callback = null) { return _rasterSource.LoadTileCoroutine(tileId, callback); } public virtual IEnumerator LoadTiles(IEnumerable tiles) { - yield return _rasterSource.LoadTilesCoroutine(GetDataId(tiles)); + // Materialize before passing to the coroutine: the IEnumerable returned by + // GetDataId is the shared _dataIdScratch set. If LoadTilesCoroutine consumes + // lazily and another GetDataId call clears the scratch before it finishes, + // the consumer would see the wrong contents. + var materialized = new List(GetDataId(tiles)); + yield return _rasterSource.LoadTilesCoroutine(materialized); } - + public IEnumerable GetTileCoverCoroutines(IEnumerable tiles) { - var targetTiles = GetDataId(tiles).Distinct(); - return targetTiles.Select(x => _rasterSource.LoadTileCoroutine(x)).Where(x => x != null); + // Same materialization rationale: callers iterate this lazily downstream and + // would otherwise read a mutated _dataIdScratch after another GetDataId call. + var materialized = new List(GetDataId(tiles)); + // Explicit lambda (not method-group): LoadTileData's optional callback + // parameter makes the method-group conversion ambiguous between Select's + // two overloads (Func vs Func). + return materialized.Select(t => LoadTileData(t)).Where(x => x != null); } #endregion - - //PRIVATE METHODS private bool IsZinSupportedRange(int targetZ) { @@ -150,7 +194,7 @@ private CanonicalTileId GetDataId(CanonicalTileId tileId) { var maxZoom = _settings.DataSettings.ClampDataLevelToMax; var currentZ = tileId.Z; - var targetZ = currentZ - 2; + var targetZ = (int)Mathf.Max(currentZ - 2, _settings.RejectTilesOutsideZoom.x); if (targetZ >= maxZoom) { return tileId.ParentAt(maxZoom); @@ -161,10 +205,100 @@ private CanonicalTileId GetDataId(CanonicalTileId tileId) } } + + //API + + /// + /// This method will only search the memory cache for available data. + /// + /// TileId of the elevation data + /// Point in texture coordinate space, origin is bottom left. + /// Point in texture coordinate space, origin is bottom left. + /// + public float QueryElevation(CanonicalTileId tileId, float x, float y) + { + if (!_settings.NeedsCpuElevation && !_elevationDisabledWarningLogged) + { + _elevationDisabledWarningLogged = true; + Debug.LogWarning("[Mapbox] QueryElevation was called but TerrainLayerModuleSettings.ExtractCpuElevationData is disabled, so no CPU elevation data exists. Returning 0 for every query. Enable ExtractCpuElevationData if anything in your scene snaps to terrain (vector features, buildings, colliders, custom elevation lookups)."); + } + + var originalTileId = tileId; + var targetTileId = tileId; + for (int i = 0; i < 5; i++) + { + if (_rasterSource.GetInstantData(targetTileId, out var instantData)) + { + return instantData.QueryHeightData(originalTileId, x, y); + } + targetTileId.MoveToParent(); + } + + return 0; + } + + /// + /// Maps a set of render tile ids to the distinct data tile ids they sample. + /// + /// + /// The returned enumerable is a reusable per-instance scratch set — the caller + /// MUST finish iterating before invoking + /// again (the next call clears the same set). Do not retain a reference past the + /// loop body. + /// public IEnumerable GetDataId(IEnumerable tileIdList) { - return tileIdList.Where(x => IsZinSupportedRange(x.Z)).Select(GetDataId).Distinct(); + // With the data-zoom offset of 2, up to 16 render tile ids collapse to the + // same data id. Reuse the scratch set to avoid allocating a HashSet plus + // Where/Select/Distinct iterator chain per call. + _dataIdScratch.Clear(); + foreach (var tileId in tileIdList) + { + if (!IsZinSupportedRange(tileId.Z)) + { + continue; + } + _dataIdScratch.Add(GetDataId(tileId)); + } + return _dataIdScratch; } + /// + /// This method will cause tile requests if data isn't already available on memory cache. Use with caution. + /// + /// Latitude longitude of the location you want to query + /// Callback method returning the elevation in float + /// + public IEnumerator GetElevationData(LatitudeLongitude latLng, Action callback = null) + { + var tileId = Conversions.LatitudeLongitudeToTileId(latLng, 14).Canonical; + var dataFound = false; + for (int i = 14; i >= 2; i--) + { + if (_rasterSource.GetInstantData(tileId, out var instantData)) + { + var tilePos = Conversions.LatitudeLongitudeToInTile01(latLng, instantData.TileId); + var elevation = instantData.QueryHeightData(tilePos); + callback?.Invoke(elevation); + dataFound = true; + break; + } + else + { + tileId = tileId.GetParentTileId; + } + } + + if (!dataFound) + { + tileId = Conversions.LatitudeLongitudeToTileId(latLng, 14).Canonical; + yield return LoadTileData(tileId, terrainData => + { + var tilePos = Conversions.LatitudeLongitudeToInTile01(latLng, terrainData.TileId); + var elevation = terrainData.QueryHeightData(tilePos); + callback?.Invoke(elevation); + }); + } + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs index 6b85cc037..367082d4d 100644 --- a/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs +++ b/Runtime/Mapbox/ImageModule/Terrain/TerrainLayerModuleSettings.cs @@ -6,17 +6,44 @@ namespace Mapbox.ImageModule.Terrain { + /// + /// User-facing settings for the . Controls how terrain + /// raster tiles are fetched, whether CPU elevation data is decoded, and how tiles are + /// rendered (shader-displaced vs. CPU-displaced meshes). + /// [Serializable] public class TerrainLayerModuleSettings { + /// Runtime-only selector for the terrain tileset (not serialized). [NonSerialized] public ElevationSourceType SourceType = ElevationSourceType.MapboxTerrain; + + [Tooltip("Pre-fetch low-zoom world tiles on startup so the map has fallback imagery while higher-zoom tiles load. Leave off to minimize cold-start bandwidth; enable for polished first-paint. Default: off.")] public bool LoadBackgroundTextures = false; + + [Tooltip("ON (recommended): the GPU vertex shader displaces a flat grid using the terrain-RGB texture — cheapest visual elevation. OFF: the CPU writes displaced vertices before render, which forces ExtractCpuElevationData on and costs more per-tile CPU. Default: on.")] public bool UseShaderTerrain = true; + + [Tooltip("Decode each terrain-RGB tile into a CPU float[] so features can query ground height. ON (default) is required for: vector snap-to-terrain (buildings, roads, POIs), terrain colliders, CPU elevation rendering, and TryGetElevation/QueryElevation. Turn OFF only for purely-terrain-rendered apps to save ~65K pixel decodes and a ~256KB float[] per tile. Force-enabled (and shown disabled) whenever UseShaderTerrain is off or colliderOptions.addCollider is on.")] + public bool ExtractCpuElevationData = true; + + [Tooltip("Tileset id, retina flag, cache size, and data-zoom clamp for the terrain raster source.")] public ImageSourceSettings DataSettings; - - [Tooltip("Tile outside this range will be rejected.")] + + [Tooltip("Tiles whose zoom level is outside this [min, max] range will be skipped. Default (12, 16) keeps terrain loaded only in typical map-viewing zooms.")] public Vector2 RejectTilesOutsideZoom = new Vector2(12, 16); + [Tooltip("Terrain mesh grid density, skirts, collider, and Unity layer assignment. See the nested fields for details.")] public ElevationLayerProperties ElevationLayerProperties = new ElevationLayerProperties(); + + /// + /// True when CPU elevation extraction is required by at least one consumer: the user + /// explicitly enabled , or shader rendering is + /// off, or a terrain collider is requested. The module force-enables extraction on + /// the raster source at Initialize time based on this value. + /// + public bool NeedsCpuElevation => + ExtractCpuElevationData + || !UseShaderTerrain + || (ElevationLayerProperties != null && ElevationLayerProperties.colliderOptions.addCollider); } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs b/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs deleted file mode 100644 index 20f31de3e..000000000 --- a/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs +++ /dev/null @@ -1,346 +0,0 @@ -using System; -using System.Collections; -using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.BaseModule.Plugins.Android.UniAndroidPermission; -using Mapbox.BaseModule.Utilities; -using Mapbox.LocationModule.AngleSmoothing; -using Mapbox.LocationModule.UnityLocationWrappers; -using Mapbox.Utils; -using UnityEngine; - -namespace Mapbox.LocationModule -{ - /// - /// The DeviceLocationProvider is responsible for providing real world location and heading data, - /// served directly from native hardware and OS. - /// This relies on Unity's LocationService for location - /// and Compass for heading. - /// - public class DeviceLocationProvider : AbstractLocationProvider - { - - - /// - /// Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. - /// Values like 5-10 could be used for getting best accuracy. - /// - [SerializeField] - [Tooltip("Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. Values like 5-10 could be used for getting best accuracy.")] - public float _desiredAccuracyInMeters = 1.0f; - - - /// - /// The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. - /// Higher values like 500 imply less overhead. - /// - [SerializeField] - [Tooltip("The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. Higher values like 500 imply less overhead.")] - public float _updateDistanceInMeters = 0.0f; - - - [SerializeField] - [Tooltip("The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")] - public long _updateTimeInMilliSeconds = 500; - - - [SerializeField] - [Tooltip("Smoothing strategy to be applied to the UserHeading.")] - public AngleSmoothingAbstractBase _userHeadingSmoothing; - - - [SerializeField] - [Tooltip("Smoothing strategy to applied to the DeviceOrientation.")] - public AngleSmoothingAbstractBase _deviceOrientationSmoothing; - - - [Serializable] - public struct DebuggingInEditor - { - [Header("Set 'EditorLocationProvider' to 'DeviceLocationProvider' and connect device with UnityRemote.")] - [SerializeField] - [Tooltip("Mock Unity's 'Input.Location' to route location log files through this class (eg fresh calculation of 'UserHeading') instead of just replaying them. To use set 'Editor Location Provider' in 'Location Factory' to 'Device Location Provider' and select a location log file below.")] - public bool _mockUnityInputLocation; - - [SerializeField] - [Tooltip("Also see above. Location log file to mock Unity's 'Input.Location'.")] - public TextAsset _locationLogFile; - } - - [Space(20)] - public DebuggingInEditor _editorDebuggingOnly; - - - private IMapboxLocationService _locationService; - private Coroutine _pollRoutine; - private double _lastLocationTimestamp; - private WaitForSeconds _wait1sec; - private WaitForSeconds _waitUpdateTime; - /// list of positions to keep for calculations - private CircularBuffer _lastPositions; - /// number of last positons to keep - private int _maxLastPositions = 5; - /// minimum needed distance between oldest and newest position before UserHeading is calculated - private double _minDistanceOldestNewestPosition = 1.5; - - - // Android 6+ permissions have to be granted during runtime - // these are the callbacks for requesting location permission - // TODO: show message to users in case they accidentallly denied permission -#if UNITY_ANDROID - private bool _gotPermissionRequestResponse = false; - - private void OnAllow() { _gotPermissionRequestResponse = true; } - private void OnDeny() { _gotPermissionRequestResponse = true; } - private void OnDenyAndNeverAskAgain() { _gotPermissionRequestResponse = true; } -#endif - - - protected virtual void Awake() - { -#if UNITY_EDITOR - if (_editorDebuggingOnly._mockUnityInputLocation) - { - if (null == _editorDebuggingOnly._locationLogFile || null == _editorDebuggingOnly._locationLogFile.bytes) - { - throw new ArgumentNullException("Location Log File"); - } - - _locationService = new MapboxLocationServiceMock(_editorDebuggingOnly._locationLogFile.bytes); - } - else - { -#endif - _locationService = new MapboxLocationServiceUnityWrapper(); -#if UNITY_EDITOR - } -#endif - - Input.location.Start(); - _currentLocation.Provider = "unity"; - _wait1sec = new WaitForSeconds(1f); - _waitUpdateTime = _updateTimeInMilliSeconds < 500 ? new WaitForSeconds(0.5f) : new WaitForSeconds((float)_updateTimeInMilliSeconds / 1000.0f); - - if (null == _userHeadingSmoothing) { _userHeadingSmoothing = transform.gameObject.AddComponent(); } - if (null == _deviceOrientationSmoothing) { _deviceOrientationSmoothing = transform.gameObject.AddComponent(); } - - _lastPositions = new CircularBuffer(_maxLastPositions); - - if (_pollRoutine == null) - { - _pollRoutine = StartCoroutine(PollLocationRoutine()); - } - } - - - /// - /// Enable location and compass services. - /// Sends continuous location and heading updates based on - /// _desiredAccuracyInMeters and _updateDistanceInMeters. - /// - /// The location routine. - IEnumerator PollLocationRoutine() - { -#if UNITY_EDITOR - while (!UnityEditor.EditorApplication.isRemoteConnected) - { - // exit if we are not the selected location provider - if (null != LocationProviderFactory.Instance && null != LocationProviderFactory.Instance.DefaultLocationProvider) - { - if (!this.Equals(LocationProviderFactory.Instance.DefaultLocationProvider)) - { - yield break; - } - } - - Debug.LogWarning("Remote device not connected via 'Unity Remote'. Waiting ..." + Environment.NewLine + "If Unity seems to be stuck here make sure 'Unity Remote' is running and restart Unity with your device already connected."); - yield return _wait1sec; - } -#endif - - - //request runtime fine location permission on Android if not yet allowed -#if UNITY_ANDROID - if (!_locationService.isEnabledByUser) - { - UniAndroidPermission.RequestPermission(AndroidPermission.ACCESS_FINE_LOCATION); - //wait for user to allow or deny - while (!_gotPermissionRequestResponse) { yield return _wait1sec; } - } -#endif - - - if (!_locationService.isEnabledByUser) - { - Debug.LogError("DeviceLocationProvider: Location is not enabled by user!"); - _currentLocation.IsLocationServiceEnabled = false; - SendLocation(_currentLocation); - yield break; - } - - - _currentLocation.IsLocationServiceInitializing = true; - _locationService.Start(_desiredAccuracyInMeters, _updateDistanceInMeters); - Input.compass.enabled = true; - - int maxWait = 20; - while (_locationService.status == LocationServiceStatus.Initializing && maxWait > 0) - { - yield return _wait1sec; - maxWait--; - } - - if (maxWait < 1) - { - Debug.LogError("DeviceLocationProvider: " + "Timed out trying to initialize location services!"); - _currentLocation.IsLocationServiceInitializing = false; - _currentLocation.IsLocationServiceEnabled = false; - SendLocation(_currentLocation); - yield break; - } - - if (_locationService.status == LocationServiceStatus.Failed) - { - Debug.LogError("DeviceLocationProvider: " + "Failed to initialize location services!"); - _currentLocation.IsLocationServiceInitializing = false; - _currentLocation.IsLocationServiceEnabled = false; - SendLocation(_currentLocation); - yield break; - } - - _currentLocation.IsLocationServiceInitializing = false; - _currentLocation.IsLocationServiceEnabled = true; - -#if UNITY_EDITOR - // HACK: this is to prevent Android devices, connected through Unity Remote, - // from reporting a location of (0, 0), initially. - yield return _wait1sec; -#endif - - System.Globalization.CultureInfo invariantCulture = System.Globalization.CultureInfo.InvariantCulture; - - while (true) - { - - var lastData = _locationService.lastData; - var timestamp = lastData.timestamp; - - /////////////////////////////// - // oh boy, Unity what are you doing??? - // on some devices it seems that - // Input.location.status != LocationServiceStatus.Running - // nevertheless new location is available - ////////////////////////////// - //Debug.LogFormat("Input.location.status: {0}", Input.location.status); - _currentLocation.IsLocationServiceEnabled = - _locationService.status == LocationServiceStatus.Running - || timestamp > _lastLocationTimestamp; - - _currentLocation.IsUserHeadingUpdated = false; - _currentLocation.IsLocationUpdated = false; - - if (!_currentLocation.IsLocationServiceEnabled) - { - yield return _waitUpdateTime; - continue; - } - - // device orientation, user heading get calculated below - _deviceOrientationSmoothing.Add(Input.compass.trueHeading); - _currentLocation.DeviceOrientation = (float)_deviceOrientationSmoothing.Calculate(); - - - //_currentLocation.LatitudeLongitude = new Vector2d(lastData.latitude, lastData.longitude); - // HACK to get back to double precision, does this even work? - // https://forum.unity.com/threads/precision-of-location-longitude-is-worse-when-longitude-is-beyond-100-degrees.133192/#post-1835164 - double latitude = double.Parse(lastData.latitude.ToString("R", invariantCulture), invariantCulture); - double longitude = double.Parse(lastData.longitude.ToString("R", invariantCulture), invariantCulture); - Vector2d previousLocation = new Vector2d(_currentLocation.LatitudeLongitude.Latitude, _currentLocation.LatitudeLongitude.Longitude); - _currentLocation.LatitudeLongitude = new LatitudeLongitude(latitude, longitude); - - _currentLocation.Accuracy = (float)Math.Floor(lastData.horizontalAccuracy); - // sometimes Unity's timestamp doesn't seem to get updated, or even jump back in time - // do an additional check if location has changed - _currentLocation.IsLocationUpdated = timestamp > _lastLocationTimestamp || !_currentLocation.LatitudeLongitude.Equals(previousLocation); - _currentLocation.Timestamp = timestamp; - _lastLocationTimestamp = timestamp; - - if (_currentLocation.IsLocationUpdated) - { - if (_lastPositions.Count > 0) - { - // only add position if user has moved +1m since we added the previous position to the list - CheapRuler cheapRuler = new CheapRuler(_currentLocation.LatitudeLongitude.Latitude, CheapRulerUnits.Meters); - var p = _currentLocation.LatitudeLongitude; - double distance = cheapRuler.Distance( - new double[] { p.Longitude, p.Latitude }, - new double[] { _lastPositions[0].Longitude, _lastPositions[0].Latitude } - ); - if (distance > 1.0) - { - _lastPositions.Add(_currentLocation.LatitudeLongitude); - } - } - else - { - _lastPositions.Add(_currentLocation.LatitudeLongitude); - } - } - - // if we have enough positions calculate user heading ourselves. - // Unity does not provide bearing based on GPS locations, just - // device orientation based on Compass.Heading. - // nevertheless, use compass for intial UserHeading till we have - // enough values to calculate ourselves. - if (_lastPositions.Count < _maxLastPositions) - { - _currentLocation.UserHeading = _currentLocation.DeviceOrientation; - _currentLocation.IsUserHeadingUpdated = true; - } - else - { - var newestPos = _lastPositions[0]; - var oldestPos = _lastPositions[_maxLastPositions - 1]; - CheapRuler cheapRuler = new CheapRuler(newestPos.Latitude, CheapRulerUnits.Meters); - // distance between last and first position in our buffer - double distance = cheapRuler.Distance( - new double[] { newestPos.Longitude, newestPos.Latitude }, - new double[] { oldestPos.Longitude, oldestPos.Latitude } - ); - // positions are minimum required distance apart (user is moving), calculate user heading - if (distance >= _minDistanceOldestNewestPosition) - { - float[] lastHeadings = new float[_maxLastPositions - 1]; - - for (int i = 1; i < _maxLastPositions; i++) - { - // atan2 increases angle CCW, flip sign of latDiff to get CW - double latDiff = -(_lastPositions[i].Latitude - _lastPositions[i - 1].Latitude); - double lngDiff = _lastPositions[i].Longitude - _lastPositions[i - 1].Longitude; - // +90.0 to make top (north) 0� - double heading = (Math.Atan2(latDiff, lngDiff) * 180.0 / Math.PI) + 90.0f; - // stay within [0..360]� range - if (heading < 0) { heading += 360; } - if (heading >= 360) { heading -= 360; } - lastHeadings[i - 1] = (float)heading; - } - - _userHeadingSmoothing.Add(lastHeadings[0]); - float finalHeading = (float)_userHeadingSmoothing.Calculate(); - - //fix heading to have 0� for north, 90� for east, 180� for south and 270� for west - finalHeading = finalHeading >= 180.0f ? finalHeading - 180.0f : finalHeading + 180.0f; - - - _currentLocation.UserHeading = finalHeading; - _currentLocation.IsUserHeadingUpdated = true; - } - } - - _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); - SendLocation(_currentLocation); - - yield return _waitUpdateTime; - } - } - } -} diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs b/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs deleted file mode 100644 index ce7c69c84..000000000 --- a/Runtime/Mapbox/LocationModule/DeviceLocationProviderAndroidNative.cs +++ /dev/null @@ -1,417 +0,0 @@ -using System; -using System.Collections; -using System.Globalization; -using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.BaseModule.Utilities; -using Mapbox.Utils; -using UnityEngine; - -namespace Mapbox.LocationModule -{ - public class DeviceLocationProviderAndroidNative : AbstractLocationProvider, IDisposable - { - - - /// - /// The minimum distance (measured in meters) a device must move laterally before location is updated. - /// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener) - /// - [SerializeField] - [Tooltip("The minimum distance (measured in meters) a device must move laterally before location is updated. Higher values like 500 imply less overhead.")] - float _updateDistanceInMeters = 0.0f; - - - /// - /// The minimum time interval between location updates, in milliseconds. - /// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener) - /// - [SerializeField] - [Tooltip("The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")] - long _updateTimeInMilliSeconds = 1000; - - - private WaitForSeconds _wait1sec; - private WaitForSeconds _wait5sec; - private WaitForSeconds _wait60sec; - /// polls location provider only at the requested update intervall to reduce load - private WaitForSeconds _waitUpdateTime; - private bool _disposed; - private static object _lock = new object(); - private Coroutine _pollLocation; - - private AndroidJavaObject _activityContext = null; - private AndroidJavaObject _gpsInstance; - private AndroidJavaObject _sensorInstance; - - - ~DeviceLocationProviderAndroidNative() { Dispose(false); } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - - protected virtual void Dispose(bool disposeManagedResources) - { - if (!_disposed) - { - if (disposeManagedResources) - { - shutdown(); - } - _disposed = true; - } - } - - - private void shutdown() - { - try - { - lock (_lock) - { - if (null != _gpsInstance) - { - _gpsInstance.Call("stopLocationListeners"); - _gpsInstance.Dispose(); - _gpsInstance = null; - } - if (null != _sensorInstance) - { - _sensorInstance.Call("stopSensorListeners"); - _sensorInstance.Dispose(); - _sensorInstance = null; - } - } - } - catch (Exception ex) - { - Debug.LogError(ex); - } - } - - - protected virtual void OnDestroy() { shutdown(); } - - - protected virtual void OnDisable() { shutdown(); } - - protected virtual void Awake() - { - // safe measures to not run when disabled or not selected as location provider - if (!enabled) { return; } - if (!transform.gameObject.activeInHierarchy) { return; } - - - _wait1sec = new WaitForSeconds(1); - _wait5sec = new WaitForSeconds(5); - _wait60sec = new WaitForSeconds(60); - // throttle if entered update intervall is unreasonably low - _waitUpdateTime = _updateTimeInMilliSeconds < 500 ? new WaitForSeconds(0.5f) : new WaitForSeconds((float)_updateTimeInMilliSeconds / 1000.0f); - - _currentLocation.IsLocationServiceEnabled = false; - _currentLocation.IsLocationServiceInitializing = true; - - if (Application.platform == RuntimePlatform.Android) - { - getActivityContext(); - getGpsInstance(true); - getSensorInstance(); - - if (_pollLocation == null) - { - _pollLocation = StartCoroutine(locationRoutine()); - } - } - } - - - private void getActivityContext() - { - using (AndroidJavaClass activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - { - _activityContext = activityClass.GetStatic("currentActivity"); - } - - if (null == _activityContext) - { - Debug.LogError("Could not get UnityPlayer activity"); - return; - } - } - - - private void getGpsInstance(bool showToastMessages = false) - { - if (null == _activityContext) { return; } - - using (AndroidJavaClass androidGps = new AndroidJavaClass("com.mapbox.android.unity.AndroidGps")) - { - if (null == androidGps) - { - Debug.LogError("Could not get class 'AndroidGps'"); - return; - } - - _gpsInstance = androidGps.CallStatic("instance", _activityContext); - if (null == _gpsInstance) - { - Debug.LogError("Could not get 'AndroidGps' instance"); - return; - } - - _activityContext.Call("runOnUiThread", new AndroidJavaRunnable(() => { _gpsInstance.Call("showMessage", "starting location listeners"); })); - - _gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds); - } - } - - - private void getSensorInstance() - { - if (null == _activityContext) { return; } - - using (AndroidJavaClass androidSensors = new AndroidJavaClass("com.mapbox.android.unity.AndroidSensors")) - { - if (null == androidSensors) - { - Debug.LogError("Could not get class 'AndroidSensors'"); - return; - } - - _sensorInstance = androidSensors.CallStatic("instance", _activityContext); - if (null == _sensorInstance) - { - Debug.LogError("Could not get 'AndroidSensors' instance"); - return; - } - - _sensorInstance.Call("startSensorListeners"); - } - } - - private IEnumerator locationRoutine() - { - - while (true) - { - // couldn't get player activity, wait and retry - if (null == _activityContext) - { - SendLocation(_currentLocation); - yield return _wait60sec; - getActivityContext(); - continue; - } - // couldn't get gps plugin instance, wait and retry - if (null == _gpsInstance) - { - SendLocation(_currentLocation); - yield return _wait60sec; - getGpsInstance(); - continue; - } - - // update device orientation - if (null != _sensorInstance) - { - _currentLocation.DeviceOrientation = _sensorInstance.Call("getOrientation"); - } - - bool locationServiceAvailable = _gpsInstance.Call("getIsLocationServiceAvailable"); - _currentLocation.IsLocationServiceEnabled = locationServiceAvailable; - - // app might have been started with location OFF but switched on after start - // check from time to time - if (!locationServiceAvailable) - { - _currentLocation.IsLocationServiceInitializing = true; - _currentLocation.Accuracy = 0; - _currentLocation.HasGpsFix = false; - _currentLocation.SatellitesInView = 0; - _currentLocation.SatellitesUsed = 0; - - SendLocation(_currentLocation); - _gpsInstance.Call("stopLocationListeners"); - yield return _wait5sec; - _gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds); - yield return _wait1sec; - continue; - } - - - // if we got till here it means location services are running - _currentLocation.IsLocationServiceInitializing = false; - - try - { - AndroidJavaObject locNetwork = _gpsInstance.Get("lastKnownLocationNetwork"); - AndroidJavaObject locGps = _gpsInstance.Get("lastKnownLocationGps"); - - // easy cases: neither or either gps location or network location available - if (null == locGps & null == locNetwork) { populateCurrentLocation(null); } - if (null != locGps && null == locNetwork) { populateCurrentLocation(locGps); } - if (null == locGps && null != locNetwork) { populateCurrentLocation(locNetwork); } - - // gps- and network location available: figure out which one to use - if (null != locGps && null != locNetwork) { populateWithBetterLocation(locGps, locNetwork); } - - - _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); - SendLocation(_currentLocation); - } - catch (Exception ex) - { - Debug.LogErrorFormat("GPS plugin error: " + ex.ToString()); - } - yield return _waitUpdateTime; - } - } - - - private void populateCurrentLocation(AndroidJavaObject location) - { - if (null == location) - { - _currentLocation.IsLocationUpdated = false; - _currentLocation.IsUserHeadingUpdated = false; - return; - } - - double lat = location.Call("getLatitude"); - double lng = location.Call("getLongitude"); - var newLatLng = new LatitudeLongitude(lat, lng); - bool coordinatesUpdated = !newLatLng.Equals(_currentLocation.LatitudeLongitude); - _currentLocation.LatitudeLongitude = newLatLng; - - float newAccuracy = location.Call("getAccuracy"); - bool accuracyUpdated = newAccuracy != _currentLocation.Accuracy; - _currentLocation.Accuracy = newAccuracy; - - // divide by 1000. Android returns milliseconds, we work with seconds - long newTimestamp = location.Call("getTime") / 1000; - bool timestampUpdated = newTimestamp != _currentLocation.Timestamp; - _currentLocation.Timestamp = newTimestamp; - - string newProvider = location.Call("getProvider"); - bool providerUpdated = newProvider != _currentLocation.Provider; - _currentLocation.Provider = newProvider; - - bool hasBearing = location.Call("hasBearing"); - // only evalute bearing when location object actually has a bearing - // Android populates bearing (which is not equal to device orientation) - // only when the device is moving. - // Otherwise it is set to '0.0' - // https://developer.android.com/reference/android/location/Location.html#getBearing() - // We don't want that when we rotate a map according to the direction - // thes user is moving, thus don't update 'heading' with '0.0' - if (!hasBearing) - { - _currentLocation.IsUserHeadingUpdated = false; - } - else - { - float newHeading = location.Call("getBearing"); - _currentLocation.IsUserHeadingUpdated = newHeading != _currentLocation.UserHeading; - _currentLocation.UserHeading = newHeading; - } - - float? newSpeed = location.Call("getSpeed"); - bool speedUpdated = newSpeed != _currentLocation.SpeedMetersPerSecond; - _currentLocation.SpeedMetersPerSecond = newSpeed; - - // flag location as updated if any of below conditions evaluates to true - // Debug.LogFormat("coords:{0} acc:{1} time:{2} speed:{3}", coordinatesUpdated, accuracyUpdated, timestampUpdated, speedUpdated); - _currentLocation.IsLocationUpdated = - providerUpdated - || coordinatesUpdated - || accuracyUpdated - || timestampUpdated - || speedUpdated; - - // Un-comment if required. Throws a warning right now. - //bool networkEnabled = _gpsInstance.Call("getIsNetworkEnabled"); - bool gpsEnabled = _gpsInstance.Call("getIsGpsEnabled"); - if (!gpsEnabled) - { - _currentLocation.HasGpsFix = null; - _currentLocation.SatellitesInView = null; - _currentLocation.SatellitesUsed = null; - } - else - { - _currentLocation.HasGpsFix = _gpsInstance.Get("hasGpsFix"); - _currentLocation.SatellitesInView = _gpsInstance.Get("satellitesInView"); - _currentLocation.SatellitesUsed = _gpsInstance.Get("satellitesUsedInFix"); - } - - } - - - /// - /// If GPS and network location are available use the newer/better one - /// - /// - /// - private void populateWithBetterLocation(AndroidJavaObject locGps, AndroidJavaObject locNetwork) - { - - // check which location is fresher - long timestampGps = locGps.Call("getTime"); - long timestampNet = locNetwork.Call("getTime"); - if (timestampGps > timestampNet) - { - populateCurrentLocation(locGps); - return; - } - - // check which location has better accuracy - float accuracyGps = locGps.Call("getAccuracy"); - float accuracyNet = locNetwork.Call("getAccuracy"); - if (accuracyGps < accuracyNet) - { - populateCurrentLocation(locGps); - return; - } - - // default to network - populateCurrentLocation(locNetwork); - } - -#if UNITY_ANDROID - - private string time2str(AndroidJavaObject loc) - { - long time = loc.Call("getTime"); - DateTime dtPlugin = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromMilliseconds(time)); - return dtPlugin.ToString("yyyyMMdd HHmmss"); - } - - - - private string loc2str(AndroidJavaObject loc) - { - - if (null == loc) { return "loc: NULL"; } - - try - { - - double lat = loc.Call("getLatitude"); - double lng = loc.Call("getLongitude"); - - return string.Format(CultureInfo.InvariantCulture, "{0:0.00000000} / {1:0.00000000}", lat, lng); - } - catch (Exception ex) - { - return ex.ToString(); - } - } - -#endif - - - - } -} diff --git a/Runtime/Mapbox/LocationModule/Editor.meta b/Runtime/Mapbox/LocationModule/Editor.meta new file mode 100644 index 000000000..595ed46e1 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ae4a372a92c1d47b69de98b0c2d7523f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs new file mode 100644 index 000000000..f4b423810 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs @@ -0,0 +1,88 @@ +using Mapbox.LocationModule; +using Mapbox.LocationModule; +using UnityEditor; +using UnityEngine; + +[CustomEditor(typeof(LocationProviderFactory))] +public class LocationProviderFactoryEditor : Editor +{ + private SerializedProperty _locationProviderTypeProp; + private SerializedProperty _mapProp; + private SerializedProperty _editorLatLngProp; + private SerializedProperty _unitySettingsProp; + private SerializedProperty _mapboxSettingsProp; + + private void OnEnable() + { + _locationProviderTypeProp = serializedObject.FindProperty("LocationProviderType"); + _editorLatLngProp = serializedObject.FindProperty("EditorLatitudeLongitude"); + _unitySettingsProp = serializedObject.FindProperty("_unityLocationProviderSettings"); + _mapboxSettingsProp = serializedObject.FindProperty("_mapboxLocationProviderSettings"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_locationProviderTypeProp); + + EditorGUILayout.Space(); + + var providerType = + (LocationProviderType)_locationProviderTypeProp.enumValueIndex; + + switch (providerType) + { + case LocationProviderType.StaticLocationProvider: + DrawEditorProvider(); + break; + + case LocationProviderType.UnityLocationProvider: + DrawUnityProvider(); + break; + + case LocationProviderType.MapboxLocationProvider: + DrawMapboxProvider(); + break; + + // case LocationProviderType.CustomLocationProvider: + // EditorGUILayout.HelpBox( + // "Custom location provider not implemented yet.", + // MessageType.Info + // ); + // break; + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawEditorProvider() + { + EditorGUILayout.HelpBox( + "Uses a fixed editor-only location.", + MessageType.None + ); + + EditorGUILayout.PropertyField(_editorLatLngProp); + } + + private void DrawUnityProvider() + { + EditorGUILayout.HelpBox( + "Uses Unity Input.location service.", + MessageType.None + ); + + EditorGUILayout.PropertyField(_unitySettingsProp); + } + + private void DrawMapboxProvider() + { + EditorGUILayout.HelpBox( + "Mapbox provider is experimental.", + MessageType.Warning + ); + + EditorGUILayout.PropertyField(_mapboxSettingsProp); + } +} diff --git a/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta new file mode 100644 index 000000000..d6c7e278d --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/LocationProviderFactoryEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fbc3b1c4861664c9a91acb80bba5f3a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef new file mode 100644 index 000000000..51fe64e76 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "MapboxLocationModule.Editor", + "rootNamespace": "", + "references": [ + "GUID:1b64b068a01f943108c7f4b7a5356c7a" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta new file mode 100644 index 000000000..e8bb3e6c8 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Editor/Mapbox.LocationModule.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6ae5860bc6f8448fc8476cc59bc698f3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/Helsinki.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/Helsinki.txt.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative-2.txt.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderAndroidNative.txt.meta diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt diff --git a/Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt.meta b/Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ExampleGpsTraces/LocationProviderUnity.txt.meta rename to Runtime/Mapbox/LocationModule/Example/ExampleGpsTraces/LocationProviderUnity.txt.meta diff --git a/Runtime/Mapbox/LocationModule/Example/LocationExample.unity b/Runtime/Mapbox/LocationModule/Example/LocationExample.unity index e94d0b173..54e9005f7 100644 --- a/Runtime/Mapbox/LocationModule/Example/LocationExample.unity +++ b/Runtime/Mapbox/LocationModule/Example/LocationExample.unity @@ -502,6 +502,7 @@ MonoBehaviour: - {fileID: 2100000, guid: 07844c7dc8ee041a398536fea1b205f0, type: 2} - {fileID: 2100000, guid: 0ed89629241ea4f1fb43ce963c79adc6, type: 2} BuildingMaterial: {fileID: 2100000, guid: 8eb65dc9490a746018cc3aa696f93965, type: 2} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} InitializeOnStart: 1 --- !u!4 &1316629400 Transform: diff --git a/Runtime/Mapbox/LocationModule/Helpers.meta b/Runtime/Mapbox/LocationModule/Helpers.meta new file mode 100644 index 000000000..7da1d74a2 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7c705c95e41449968c82bd5b9ba4484 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs new file mode 100644 index 000000000..40f3f734b --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs @@ -0,0 +1,51 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.LocationModule; +using UnityEngine; + +namespace Mapbox.LocationModule.Helpers +{ + public class ShiftMapForDistance : MonoBehaviour + { + public MapBehaviourCore Map; + public LocationProviderFactory LocationProviderFactory; + [Tooltip("Shift whole map if the distance between last two location points is bigger than this value (in mercator units). ")] + public float ShiftMapDistance; + + private MapboxMap _map; + private ILocationProvider _locationProvider; + private LatitudeLongitude _previousLocation = LatitudeLongitude.Invalid; + private LatitudeLongitude _currentLocation; + + private void Start() + { + Map.Initialized += map => + { + _map = map; + _locationProvider = LocationProviderFactory.DefaultLocationProvider; + if (_locationProvider != null) + { + _locationProvider.OnLocationUpdated += LocationUpdated; + } + }; + } + + private void LocationUpdated(Location location) + { + _currentLocation = location.LatitudeLongitude; + if (_previousLocation != LatitudeLongitude.Invalid) + { + var d1 = Conversions.LatitudeLongitudeToWebMercator(_previousLocation); + var d2 = Conversions.LatitudeLongitudeToWebMercator(_currentLocation); + if (Vector2d.Distance(d1, d2) > ShiftMapDistance) + { + Debug.Log(string.Format("ShiftMapForDistance: d1: {0} d2: {1}", d1, d2)); + _map.ChangeView(_currentLocation); + } + } + + _previousLocation = _currentLocation; + } + } +} diff --git a/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs.meta b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs.meta new file mode 100644 index 000000000..d415bb5b9 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Helpers/ShiftMapForDistance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5534c9989da84262bc64ffe27afbbb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs b/Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs similarity index 67% rename from Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs index eb70b7641..2052ac8b9 100644 --- a/Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs @@ -1,10 +1,10 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.Map; using Mapbox.LocationModule; using UnityEngine; +//using Mapbox.LocationModule; -namespace Mapbox.Example.Scripts.LocationBehaviours +namespace Mapbox.LocationModule.Helpers { public class SnapTransformToLocationProvider : MonoBehaviour { @@ -19,20 +19,20 @@ private void Start() { if(_transform == null) _transform = transform; - - if(_locationProvider == null) + + if (_locationProvider == null) + { Debug.Log("_locationProvider null"); - - if(_locationProvider.DefaultLocationProvider == null) - Debug.Log("DefaultLocationProvider null"); - - if(!enabled || _locationProvider == null || _locationProvider.DefaultLocationProvider == null) return; - + } + _mapCore.Initialized += (map) => { _map = map; - _locationProvider.DefaultLocationProvider.OnLocationUpdated += OnDefaultLocationProviderOnOnLocationUpdated; + if (_locationProvider.DefaultLocationProvider != null) + { + _locationProvider.DefaultLocationProvider.OnLocationUpdated += OnDefaultLocationProviderOnOnLocationUpdated; + } }; } diff --git a/Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs.meta similarity index 100% rename from Samples~/LocationBasedGame/Scripts/SnapTransformToLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Helpers/SnapTransformToLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs b/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs deleted file mode 100644 index f98c7aa49..000000000 --- a/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Text.RegularExpressions; -using UnityEngine; - -namespace Mapbox.LocationModule -{ - /// - /// Singleton factory to allow easy access to various LocationProviders. - /// This is meant to be attached to a game object. - /// - public class LocationProviderFactory : MonoBehaviour - { - [SerializeField] - [Tooltip("Provider using Unity's builtin 'Input.Location' service")] - AbstractLocationProvider _deviceLocationProviderUnity; - - [SerializeField] - [Tooltip("Custom native Android location provider. If this is not set above provider is used")] - DeviceLocationProviderAndroidNative _deviceLocationProviderAndroid; - - [SerializeField] - AbstractLocationProvider _editorLocationProvider; - - [SerializeField] - AbstractLocationProvider _transformLocationProvider; - - [SerializeField] - bool _dontDestroyOnLoad; - - - /// - /// The singleton instance of this factory. - /// - private static LocationProviderFactory _instance; - public static LocationProviderFactory Instance - { - get - { - return _instance; - } - - private set - { - _instance = value; - } - } - - ILocationProvider _defaultLocationProvider; - - /// - /// The default location provider. - /// Outside of the editor, this will be a . - /// In the Unity editor, this will be an - /// - /// - /// Fetch location to set a transform's position: - /// - /// void Update() - /// { - /// var locationProvider = LocationProviderFactory.Instance.DefaultLocationProvider; - /// transform.position = Conversions.GeoToWorldPosition(locationProvider.Location, - /// MapController.ReferenceTileRect.Center, - /// MapController.WorldScaleFactor).ToVector3xz(); - /// } - /// - /// - public ILocationProvider DefaultLocationProvider - { - get - { - return _defaultLocationProvider; - } - set - { - _defaultLocationProvider = value; - } - } - - /// - /// Returns the serialized . - /// - public ILocationProvider TransformLocationProvider - { - get - { - return _transformLocationProvider; - } - } - - /// - /// Returns the serialized . - /// - public ILocationProvider EditorLocationProvider - { - get - { - return _editorLocationProvider; - } - } - - /// - /// Returns the serialized - /// - public ILocationProvider DeviceLocationProvider - { - get - { - return _deviceLocationProviderUnity; - } - } - - /// - /// Create singleton instance and inject the DefaultLocationProvider upon initialization of this component. - /// - protected virtual void Awake() - { - if (Instance != null) - { - DestroyImmediate(gameObject); - return; - } - Instance = this; - - if (_dontDestroyOnLoad) - { - DontDestroyOnLoad(gameObject); - } - - #if UNITY_EDITOR - Debug.LogFormat("LocationProviderFactory: Injected EDITOR Location Provider - {0}", _editorLocationProvider.GetType()); - DefaultLocationProvider = _editorLocationProvider; - #else - Debug.LogFormat("LocationProviderFactory: Injected DEVICE Location Provider - {0}", _deviceLocationProviderUnity.GetType()); - DefaultLocationProvider = _deviceLocationProviderUnity; - #endif - // InjectEditorLocationProvider(); - // InjectDeviceLocationProvider(); - } - - /// - /// Injects the editor location provider. - /// Depending on the platform, this method and calls to it will be stripped during compile. - /// - [System.Diagnostics.Conditional("UNITY_EDITOR")] - void InjectEditorLocationProvider() - { - Debug.LogFormat("LocationProviderFactory: Injected EDITOR Location Provider - {0}", _editorLocationProvider.GetType()); - DefaultLocationProvider = _editorLocationProvider; - } - - /// - /// Injects the device location provider. - /// Depending on the platform, this method and calls to it will be stripped during compile. - /// - [System.Diagnostics.Conditional("NOT_UNITY_EDITOR")] - void InjectDeviceLocationProvider() - { - int AndroidApiVersion = 0; - var regex = new Regex(@"(?<=API-)-?\d+"); - Match match = regex.Match(SystemInfo.operatingSystem); // eg 'Android OS 8.1.0 / API-27 (OPM2.171019.029/4657601)' - if (match.Success) { int.TryParse(match.Groups[0].Value, out AndroidApiVersion); } - Debug.LogFormat("{0} => API version: {1}", SystemInfo.operatingSystem, AndroidApiVersion); - - // // only inject native provider if platform requirement is met - // // and script itself as well as parent game object are active - // if (Application.platform == RuntimePlatform.Android - // && null != _deviceLocationProviderAndroid - // && _deviceLocationProviderAndroid.enabled - // && _deviceLocationProviderAndroid.transform.gameObject.activeInHierarchy - // // API version 24 => Android 7 (Nougat): we are using GnssStatus 'https://developer.android.com/reference/android/location/GnssStatus.html' - // // in the native plugin. - // // GnssStatus is not available with versions lower than 24 - // && AndroidApiVersion >= 24 - // ) - // { - // Debug.LogFormat("LocationProviderFactory: Injected native Android DEVICE Location Provider - {0}", _deviceLocationProviderAndroid.GetType()); - // DefaultLocationProvider = _deviceLocationProviderAndroid; - // } - // else - { - Debug.LogFormat("LocationProviderFactory: Injected DEVICE Location Provider - {0}", _deviceLocationProviderUnity.GetType()); - DefaultLocationProvider = _deviceLocationProviderUnity; - } - } - } -} diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation.meta b/Runtime/Mapbox/LocationModule/MapboxLocation.meta new file mode 100644 index 000000000..1f9ba6b88 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e6aa80deedf24c4a84fc47aa793609a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs new file mode 100644 index 000000000..01b35dffd --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs @@ -0,0 +1,341 @@ +#if !UNITY_EDITOR && UNITY_ANDROID + +using System; +using System.Collections.Generic; +using Mapbox.LocationModule; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + [Preserve] + public class MapboxLocationAndroid : IMapboxDeviceLocation + { + [Preserve] public event Action LocationUpdated; + [Preserve] public event Action AuthorizationChanged; + [Preserve] public event Action AccuracyAuthorizationChanged; + [Preserve] public event Action AvailabilityChanged; + + public MapboxLocationSettings _mapboxLocationSettings; + + private readonly Queue _mainThreadQueue = new Queue(); + private readonly object _queueLock = new object(); + + private AndroidJavaObject _permissionsManager; + private AndroidJavaObject _unityActivity; + + private string _mapboxLocationServiceFactoryClassName = "com.mapbox.common.location.LocationServiceFactory"; + private string _mapboxLocationServiceFactoryGetMethodName = "getOrCreate"; + private string _mapboxLocationServiceGetProviderMethodName = "getDeviceLocationProvider"; + private string _mapboxLocationProviderRequestClassName = "com.mapbox.common.location.LocationProviderRequest"; + private string _mapboxLocationInvervalClassName = "com.mapbox.common.location.IntervalSettings"; + + + private AndroidJavaClass _locationServiceFactory; + private AndroidJavaObject _locationService; + private AndroidJavaObject _locationProvider; + private MapboxLocationObserverProxy _locationObserver; + private MapboxLocationServiceObserverProxy _serviceObserver; + private AndroidJavaObject _accuracyLevelHigh; + + private float _lastLocationPollTime; + private float _locationPollInterval = 1.0f; // Poll every 1 second + + private string _intervalSettingsBuilderClassName = "com.mapbox.common.location.IntervalSettings$Builder"; + private string _minimumIntervalFieldName = "minimumInterval"; + private string _maximumIntervalFieldName = "maximumInterval"; + private string _intervalFieldName = "interval"; + + private string _locationProviderRequestBuilderClassName = "com.mapbox.common.location.LocationProviderRequest$Builder"; + + private string _accuracyLevelClassName = "com.mapbox.common.location.AccuracyLevel"; + private string _accuracyFieldName = "accuracy"; + private string _displacementFieldName = "displacement"; + private string _intervalSettingFieldName = "interval"; + + private string _addServiceObserverMethodName = "registerObserver"; + private string _addObserverMethodName = "addLocationObserver"; + + private string javaLangLong = "java.lang.Long"; + private string javaLangFloat = "java.lang.Float"; + + // Android class names + private const string _unityPlayerClassName = "com.unity3d.player.UnityPlayer"; + private const string _permissionsManagerClassName = "com.mapbox.android.core.permissions.PermissionsManager"; + private const string _packageManagerClassName = "android.content.pm.PackageManager"; + private const string _deviceLocationProviderTypeClassName = "com.mapbox.common.location.DeviceLocationProviderType"; + + // Android method names + private const string _currentActivityFieldName = "currentActivity"; + private const string _areLocationPermissionsGrantedMethodName = "areLocationPermissionsGranted"; + private const string _requestLocationPermissionsMethodName = "requestLocationPermissions"; + private const string _buildMethodName = "build"; + private const string _isValueMethodName = "isValue"; + private const string _getErrorMethodName = "getError"; + private const string _toStringMethodName = "toString"; + private const string _getValueMethodName = "getValue"; + private const string _valuesMethodName = "values"; + private const string _getLastLocationMethodName = "getLastLocation"; + private const string _checkSelfPermissionMethodName = "checkSelfPermission"; + private const string _getClassMethodName = "getClass"; + private const string _getNameMethodName = "getName"; + + + // Android constants + private const string _permissionGrantedFieldName = "PERMISSION_GRANTED"; + private const string _fineLocationPermission = "android.permission.ACCESS_FINE_LOCATION"; + private const string _coarseLocationPermission = "android.permission.ACCESS_COARSE_LOCATION"; + private const string _androidProviderTypeName = "ANDROID"; + + // Log prefix + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + + public MapboxLocationAndroid(MapboxLocationSettings settings) + { + _mapboxLocationSettings = settings; + } + + public void Initialize() + { + //Get Unity activity + using (var unityPlayer = new AndroidJavaClass(_unityPlayerClassName)) + { + _unityActivity = unityPlayer.GetStatic(_currentActivityFieldName); + } + + InitializeMapboxLocation(); + } + + public void Update() + { + // Poll location at intervals + if (_locationProvider != null && Time.realtimeSinceStartup - _lastLocationPollTime >= _locationPollInterval) + { + _lastLocationPollTime = Time.realtimeSinceStartup; + //GetLastLocation(); + } + + // Process main thread queue + lock (_queueLock) + { + while (_mainThreadQueue.Count > 0) + { + var action = _mainThreadQueue.Dequeue(); + try + { + action?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Error processing main thread action: {ex.Message}"); + } + } + } + } + + public void OnDestroy() + { + try + { + // Clean up location provider + if (_locationProvider != null) + { + _locationProvider.Dispose(); + _locationProvider = null; + } + + // Clean up location service + if (_locationService != null) + { + _locationService.Dispose(); + _locationService = null; + } + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Error during cleanup: {ex.Message}"); + } + } + + + + private void RequestLocationPermissions() + { + var permissionsListenerProxy = new MapboxLocationPermissionsListenerProxy( + onExplanation: (permissions) => + { + }, + onResult: (granted) => + { + if (granted) + { + InitializeMapboxLocation(); + } + else + { + Debug.LogError(_logPrefix + "Location permissions denied"); + } + }); + + _permissionsManager = new AndroidJavaObject(_permissionsManagerClassName, permissionsListenerProxy); + _permissionsManager.Call(_requestLocationPermissionsMethodName, _unityActivity); + } + + private void InitializeMapboxLocation() + { + _locationServiceFactory = new AndroidJavaClass(_mapboxLocationServiceFactoryClassName); + _locationService = _locationServiceFactory.CallStatic(_mapboxLocationServiceFactoryGetMethodName); + + var intervalSettings = new AndroidJavaObject(_intervalSettingsBuilderClassName) + .Call(_minimumIntervalFieldName, + new AndroidJavaObject(javaLangLong, _mapboxLocationSettings.MinimumInterval)) + .Call(_maximumIntervalFieldName, + new AndroidJavaObject(javaLangLong, _mapboxLocationSettings.MaximumInterval)) + .Call(_intervalFieldName, + new AndroidJavaObject(javaLangLong, _mapboxLocationSettings.Interval)) + .Call(_buildMethodName); + + GetAccuracyLevel(); + + var requestSettings = new AndroidJavaObject(_locationProviderRequestBuilderClassName) + .Call(_accuracyFieldName, _accuracyLevelHigh) + .Call(_displacementFieldName, + new AndroidJavaObject(javaLangFloat, _mapboxLocationSettings.Displacement)) + .Call(_intervalSettingFieldName, intervalSettings) + .Call(_buildMethodName); + + //AndroidJavaObject nullRequest = null; + var androidType = GetProviderType(); + var expected = _locationService.Call(_mapboxLocationServiceGetProviderMethodName, androidType, requestSettings, true); + var hasValue = expected.Call(_isValueMethodName); + + if (!hasValue) + { + AndroidJavaObject error = expected.Call(_getErrorMethodName); + Debug.LogError(_logPrefix + "Mapbox error: " + error.Call(_toStringMethodName)); + return; + } + + _serviceObserver = new MapboxLocationServiceObserverProxy(AuthorizationChanged, AccuracyAuthorizationChanged, AvailabilityChanged, EnqueueOnMainThread); + + try + { + _locationService.Call(_addServiceObserverMethodName, _serviceObserver); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Failed to register service observer: {ex.Message}"); + } + + _locationProvider = expected.Call(_getValueMethodName); + _locationObserver = new MapboxLocationObserverProxy(EnqueueOnMainThread, LocationUpdated); + + try + { + // Keep a strong reference to prevent GC + System.GC.KeepAlive(_locationObserver); + + _locationProvider.Call(_addObserverMethodName, _locationObserver); + + // Verify Android location permissions + try + { + var permissionClass = new AndroidJavaClass(_packageManagerClassName); + int permissionGranted = permissionClass.GetStatic(_permissionGrantedFieldName); + + int fineStatus = _unityActivity.Call(_checkSelfPermissionMethodName, _fineLocationPermission); + int coarseStatus = _unityActivity.Call(_checkSelfPermissionMethodName, _coarseLocationPermission); + + if (fineStatus != permissionGranted && coarseStatus != permissionGranted) + { + Debug.LogError(_logPrefix + "No location permissions! This is why observer won't receive updates."); + } + } + catch (Exception permEx) + { + Debug.LogWarning(_logPrefix + $"Could not verify permissions: {permEx.Message}"); + } + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Failed to add location observer: {ex.Message}\n{ex.StackTrace}"); + } + + GetLastLocation(); + } + + private void GetLastLocation() + { + try + { + var callback = new GetLocationCallbackProxy((location, isValid) => + { + if (isValid) + { + EnqueueOnMainThread(() => + { + LocationUpdated?.Invoke(location); + }); + } + }); + + _locationProvider.Call(_getLastLocationMethodName, callback); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"getLastLocation failed: {ex.Message}"); + } + } + + private void EnqueueOnMainThread(Action action) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(action); + } + } + + private void GetAccuracyLevel() + { + var accuracyLevel = new AndroidJavaClass(_accuracyLevelClassName); + var accuArray = accuracyLevel.CallStatic(_valuesMethodName); + var convertedArray = AndroidJNIHelper.ConvertFromJNIArray(accuArray.GetRawObject()); + foreach (var javaEnumObject in convertedArray) + { + var enumString = javaEnumObject.Call(_toStringMethodName); + if (enumString == _mapboxLocationSettings.AccuracyLevel.ToString()) + { + _accuracyLevelHigh = javaEnumObject; + break; + } + } + } + + private AndroidJavaObject GetProviderType() + { + var accuracyLevel = new AndroidJavaClass(_deviceLocationProviderTypeClassName); + var accuArray = accuracyLevel.CallStatic(_valuesMethodName); + var convertedArray = AndroidJNIHelper.ConvertFromJNIArray(accuArray.GetRawObject()); + foreach (var javaEnumObject in convertedArray) + { + var enumString = javaEnumObject.Call(_toStringMethodName); + if (enumString == _androidProviderTypeName) + { + return javaEnumObject; + } + } + + return null; + } + + private string GetJavaClassName(AndroidJavaObject obj) + { + if (obj == null) + return ""; + + using var clazz = obj.Call(_getClassMethodName); + return clazz.Call(_getNameMethodName); + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta new file mode 100644 index 000000000..1d43e966e --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationAndroid.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ccd02e95980847b3940f678ce4f137ff +timeCreated: 1769177065 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs new file mode 100644 index 000000000..2c0f9ee7a --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using UnityEngine; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; + +#if UNITY_IOS +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationIos : IMapboxDeviceLocation + { + public event Action LocationUpdated = delegate { }; + public event Action AuthorizationChanged = delegate { }; + public event Action AccuracyAuthorizationChanged = delegate { }; + public event Action AvailabilityChanged = delegate { }; + + public MapboxLocationSettings _mapboxLocationSettings; + + private Location _currentLocation; + // Static reference for IL2CPP callback compatibility + private static MapboxLocationIos _instance; + private GCHandle _gcHandle; + + // Queue for marshaling callbacks to main thread + private Queue _mainThreadQueue = new Queue(); + private object _queueLock = new object(); + + // Callback delegate for location updates + private delegate void LocationUpdateCallback( + double latitude, double longitude, + float accuracy, double timestamp, + double altitude, float speed, float bearing); + + // Callback delegates for service observer + private delegate void AuthorizationStatusCallback(int status); + private delegate void AccuracyAuthorizationCallback(int accuracy); + private delegate void AvailabilityCallback(bool available); + + // DllImport declarations + [DllImport("__Internal")] + private static extern void requestLocationAuthorization(); + + [DllImport("__Internal")] + private static extern IntPtr startLocationUpdatesWithSettings( + long minimumInterval, long maximumInterval, long interval, + int accuracyLevel, float displacement, + LocationUpdateCallback callback); + + [DllImport("__Internal")] + private static extern void stopLocationUpdates(IntPtr provider); + + [DllImport("__Internal")] + private static extern void addLocationServiceObserver( + AuthorizationStatusCallback authCallback, + AccuracyAuthorizationCallback accuracyCallback, + AvailabilityCallback availabilityCallback); + + [DllImport("__Internal")] + private static extern void removeLocationServiceObserver(); + + // Implementation details + private IntPtr _locationProvider; + private static LocationUpdateCallback _callback; + private static AuthorizationStatusCallback _authCallback; + private static AccuracyAuthorizationCallback _accuracyCallback; + private static AvailabilityCallback _availabilityCallback; + private bool _isStarted = false; + private bool _observerAdded = false; + + public MapboxLocationIos(MapboxLocationSettings settings) + { + _mapboxLocationSettings = settings; + // Request location authorization first + requestLocationAuthorization(); + + // Replace the static instance first so native callbacks always have a + // valid target, then tear down the old instance's native resources. + var previous = _instance; + _instance = this; + _gcHandle = GCHandle.Alloc(this); + + if (previous != null) + { + Debug.LogWarning("MapboxLocationIos: Replacing existing instance. Cleaning up previous native resources."); + previous.StopNativeUpdates(); + } + + // Create static callback delegates + _callback = OnLocationUpdateStatic; + _authCallback = OnAuthorizationChangedStatic; + _accuracyCallback = OnAccuracyAuthorizationChangedStatic; + _availabilityCallback = OnAvailabilityChangedStatic; + + // Add service observer for permission changes + addLocationServiceObserver(_authCallback, _accuracyCallback, _availabilityCallback); + _observerAdded = true; + + // Map accuracy level enum to integer + int accuracyLevel = (int)_mapboxLocationSettings.AccuracyLevel; + + // Start location updates with settings + _locationProvider = startLocationUpdatesWithSettings( + _mapboxLocationSettings.MinimumInterval, + _mapboxLocationSettings.MaximumInterval, + _mapboxLocationSettings.Interval, + accuracyLevel, + _mapboxLocationSettings.Displacement, + _callback); + + if (_locationProvider != IntPtr.Zero) + { + _isStarted = true; + + // Initialize location struct + _currentLocation.IsLocationServiceInitializing = false; + _currentLocation.IsLocationServiceEnabled = true; + _currentLocation.Provider = "MapboxCommon"; + _currentLocation.ProviderClass = "LocationIos"; + } + else + { + Debug.LogError("Failed to start iOS location service"); + _currentLocation.IsLocationServiceInitializing = false; + _currentLocation.IsLocationServiceEnabled = false; + } + } + + public void Initialize() + { + + } + + // Static callback for IL2CPP compatibility + [AOT.MonoPInvokeCallback(typeof(LocationUpdateCallback))] + private static void OnLocationUpdateStatic( + double latitude, double longitude, + float accuracy, double timestamp, + double altitude, float speed, float bearing) + { + if (_instance != null) + { + _instance.OnLocationUpdate(latitude, longitude, accuracy, timestamp, altitude, speed, bearing); + } + } + + // Static callback for authorization status changes + [AOT.MonoPInvokeCallback(typeof(AuthorizationStatusCallback))] + private static void OnAuthorizationChangedStatic(int status) + { + if (_instance != null) + { + _instance.OnAuthorizationChanged(status); + } + } + + // Static callback for accuracy authorization changes + [AOT.MonoPInvokeCallback(typeof(AccuracyAuthorizationCallback))] + private static void OnAccuracyAuthorizationChangedStatic(int accuracy) + { + if (_instance != null) + { + _instance.OnAccuracyAuthorizationChanged(accuracy); + } + } + + // Static callback for availability changes + [AOT.MonoPInvokeCallback(typeof(AvailabilityCallback))] + private static void OnAvailabilityChangedStatic(bool available) + { + if (_instance != null) + { + _instance.OnAvailabilityChanged(available); + } + } + + // Instance method to handle the actual update + private void OnLocationUpdate(double latitude, double longitude, float accuracy, double timestamp, double altitude, float speed, float bearing) + { + // Queue the update to be processed on the main thread + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + // Update location struct + _currentLocation.LatitudeLongitude = new LatitudeLongitude(latitude, longitude); + _currentLocation.Accuracy = accuracy; + _currentLocation.Timestamp = timestamp; + _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); + + // Set optional properties + _currentLocation.SpeedMetersPerSecond = speed > 0 ? speed : (float?)null; + _currentLocation.UserHeading = bearing; + _currentLocation.IsUserHeadingUpdated = bearing > 0; + + _currentLocation.IsLocationUpdated = true; + _currentLocation.IsLocationServiceEnabled = true; + _currentLocation.IsLocationServiceInitializing = false; + + // Send location update event + LocationUpdated(_currentLocation); + } + catch (Exception ex) + { + Debug.LogError($"Exception in iOS location update: {ex.Message}"); + } + }); + } + } + + // Instance method to handle authorization status changes + private void OnAuthorizationChanged(int status) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + AuthorizationChanged((MapboxLocationServiceStatus)status); + } + catch (Exception ex) + { + Debug.LogError($"Exception in authorization changed: {ex.Message}"); + } + }); + } + } + + // Instance method to handle accuracy authorization changes + private void OnAccuracyAuthorizationChanged(int accuracy) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + AccuracyAuthorizationChanged((AccuracyAuthorization)accuracy); + } + catch (Exception ex) + { + Debug.LogError($"Exception in accuracy authorization changed: {ex.Message}"); + } + }); + } + } + + // Instance method to handle availability changes + private void OnAvailabilityChanged(bool available) + { + lock (_queueLock) + { + _mainThreadQueue.Enqueue(() => + { + try + { + AvailabilityChanged(available); + } + catch (Exception ex) + { + Debug.LogError($"Exception in availability changed: {ex.Message}"); + } + }); + } + } + + public void Update() + { + // Process queued location updates on the main thread + lock (_queueLock) + { + while (_mainThreadQueue.Count > 0) + { + var action = _mainThreadQueue.Dequeue(); + action?.Invoke(); + } + } + } + + /// + /// Stop native location updates and free the GCHandle. + /// Does not touch the static _instance — safe to call on a replaced instance. + /// + private void StopNativeUpdates() + { + if (_isStarted && _locationProvider != IntPtr.Zero) + { + stopLocationUpdates(_locationProvider); + _isStarted = false; + } + + if (_observerAdded) + { + removeLocationServiceObserver(); + _observerAdded = false; + } + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + } + + public void OnDestroy() + { + StopNativeUpdates(); + + // Clear the static reference only if it still points to this instance. + // If another instance replaced us, leave it alone — it owns the callbacks now. + if (_instance == this) + { + _instance = null; + _callback = null; + _authCallback = null; + _accuracyCallback = null; + _availabilityCallback = null; + } + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta new file mode 100644 index 000000000..bdab74221 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationIos.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1a846f70d4024ec4b41689510701521 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs new file mode 100644 index 000000000..a44f338dd --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs @@ -0,0 +1,106 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.LocationModule; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationObserverProxy : AndroidJavaProxy + { + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + private const string _getLatitude = "getLatitude"; + private const string _getLongitude = "getLongitude"; + private const string _getAltitude = "getAltitude"; + private const string _getSpeed = "getSpeed"; + private const string _getBearing = "getBearing"; + private const string _get = "get"; + private const string _size = "size"; + + public Action SendLocation; + private readonly Action _enqueueOnMainThread; + + public MapboxLocationObserverProxy(Action enqueueOnMainThread, Action locationUpdated) : base("com.mapbox.common.location.LocationObserver") + { + _enqueueOnMainThread = enqueueOnMainThread; + SendLocation = locationUpdated; + } + + [Preserve] + public void onLocationUpdateReceived(AndroidJavaObject locations) + { + if (locations == null) + { + Debug.LogWarning(_logPrefix + "locations list is null"); + return; + } + + try + { + var size = locations.Call(_size); + for (var i = 0; i < size; i++) + { + var loc = locations.Call(_get, i); + + if (loc == null) + { + Debug.LogWarning(_logPrefix + $"Location at index {i} is null"); + continue; + } + + var lat = loc.Call(_getLatitude); + var lon = loc.Call(_getLongitude); + + // Get optional properties - Mapbox returns @Nullable Double for these + // double? altitude = null; + // float? speed = null; + // float? bearing = null; + // + // // Get altitude - returns null if not available + // var altitudeObj = loc.Call(_getAltitude); + // if (altitudeObj != null) + // { + // altitude = altitudeObj.Call("doubleValue"); + // } + // + // // Get speed - returns null if not available + // var speedObj = loc.Call(_getSpeed); + // if (speedObj != null) + // { + // speed = (float)speedObj.Call("doubleValue"); + // } + // + // // Get bearing - returns null if not available + // var bearingObj = loc.Call(_getBearing); + // if (bearingObj != null) + // { + // bearing = (float)bearingObj.Call("doubleValue"); + // } + + var location = new Location + { + LatitudeLongitude = new LatitudeLongitude(lat, lon), + TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow), + IsLocationUpdated = true, + IsLocationServiceEnabled = true, + //Altitude = altitude, + //SpeedMetersPerSecond = speed, + //Bearing = bearing + }; + + // Invoke on main thread + _enqueueOnMainThread?.Invoke(() => + { + SendLocation?.Invoke(location); + }); + } + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"Error in onLocationUpdateReceived: {ex.Message}\n{ex.StackTrace}"); + } + } + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta new file mode 100644 index 000000000..cfd75ec5c --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationObserverProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ddbbcd5894704384b563164b1a7d16c0 +timeCreated: 1770213228 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs new file mode 100644 index 000000000..379aeb157 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs @@ -0,0 +1,39 @@ +using System; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationPermissionsListenerProxy : AndroidJavaProxy + { + private readonly Action _onExplanation; + private readonly Action _onResult; + + public MapboxLocationPermissionsListenerProxy(Action onExplanation, Action onResult) : base("com.mapbox.android.core.permissions.PermissionsListener") + { + _onExplanation = onExplanation; + _onResult = onResult; + } + + [Preserve] + public void onExplanationNeeded(AndroidJavaObject permissionsToExplain) + { + if (permissionsToExplain != null) + { + int size = permissionsToExplain.Call("size"); + string[] permissions = new string[size]; + for (int i = 0; i < size; i++) + { + permissions[i] = permissionsToExplain.Call("get", i); + } + _onExplanation?.Invoke(permissions); + } + } + + [Preserve] + public void onPermissionResult(bool granted) + { + _onResult?.Invoke(granted); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta new file mode 100644 index 000000000..263436246 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationPermissionsListenerProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 73879c85c25049ba8e25c629b265fc1b +timeCreated: 1770213220 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs new file mode 100644 index 000000000..8170715a3 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs @@ -0,0 +1,49 @@ +using System; +using Mapbox.LocationModule; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationProvider : AbstractLocationProvider + { + public event Action AuthorizationChanged; + public event Action AccuracyAuthorizationChanged; + public event Action AvailabilityChanged; + + public MapboxLocationSettings Settings; + private IMapboxDeviceLocation _mapboxDeviceLocation; + + public MapboxLocationProvider(MapboxLocationSettings settings) + { + Settings = settings; +#if UNITY_IOS && !UNITY_EDITOR + _mapboxDeviceLocation = new MapboxLocationIos(Settings); +#elif UNITY_ANDROID && !UNITY_EDITOR + _mapboxDeviceLocation = new MapboxLocationAndroid(Settings); +#endif + + if (_mapboxDeviceLocation != null) + { + _mapboxDeviceLocation.LocationUpdated += (s) => + { + _currentLocation = s; + SendLocation(s); + }; + _mapboxDeviceLocation.AvailabilityChanged += (v) => AvailabilityChanged?.Invoke(v); + _mapboxDeviceLocation.AuthorizationChanged += (v) => AuthorizationChanged?.Invoke(v); + _mapboxDeviceLocation.AccuracyAuthorizationChanged += (v) => AccuracyAuthorizationChanged?.Invoke(v); + + _mapboxDeviceLocation.Initialize(); + } + } + + public override void Update() + { + _mapboxDeviceLocation?.Update(); + } + + public override void OnDestroy() + { + _mapboxDeviceLocation?.OnDestroy(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta new file mode 100644 index 000000000..691de9699 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 340989a2b4f14f6a823b107af7c1aacc +timeCreated: 1769267389 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs new file mode 100644 index 000000000..dfb0aa4d0 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs @@ -0,0 +1,63 @@ +using System; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule.MapboxLocation +{ + public class MapboxLocationServiceObserverProxy : AndroidJavaProxy + { + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + + private readonly Action _authorizationChanged; + private readonly Action _accuracyAuthorizationChanged; + private readonly Action _availabilityChanged; + private readonly Action _enqueueOnMainThread; + + public MapboxLocationServiceObserverProxy( + Action authorizationChanged, + Action accuracyAuthorizationChanged, + Action availabilityChanged, + Action enqueueOnMainThread) : base("com.mapbox.common.location.LocationServiceObserver") + { + _authorizationChanged = authorizationChanged; + _accuracyAuthorizationChanged = accuracyAuthorizationChanged; + _availabilityChanged = availabilityChanged; + _enqueueOnMainThread = enqueueOnMainThread; + } + + + [Preserve] + public void onAvailabilityChanged(bool isAvailable) + { + _enqueueOnMainThread?.Invoke(() => _availabilityChanged?.Invoke(isAvailable)); + } + + [Preserve] + public void onPermissionStatusChanged(AndroidJavaObject permission) + { + if (permission == null) + { + Debug.LogError(_logPrefix + "onPermissionStatusChanged received null"); + return; + } + + int ordinal = permission.Call("ordinal"); + var status = (MapboxLocationServiceStatus)ordinal; + _enqueueOnMainThread?.Invoke(() => _authorizationChanged?.Invoke(status)); + } + + [Preserve] + public void onAccuracyAuthorizationChanged(AndroidJavaObject authorization) + { + if (authorization == null) + { + Debug.LogError(_logPrefix + "onAccuracyAuthorizationChanged received null"); + return; + } + + int ordinal = authorization.Call("ordinal"); + var accuracy = (AccuracyAuthorization)ordinal; + _enqueueOnMainThread?.Invoke(() => _accuracyAuthorizationChanged?.Invoke(accuracy)); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta new file mode 100644 index 000000000..22c988320 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationServiceObserverProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: acb662a539c346beafce8455dc4ab3c0 +timeCreated: 1770213213 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs new file mode 100644 index 000000000..d85da597c --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs @@ -0,0 +1,84 @@ +using System; +using UnityEngine; + +namespace Mapbox.LocationModule.MapboxLocation +{ + [Serializable] + public class MapboxLocationSettings + { + // The accuracy of the observed location + [Tooltip("The accuracy of the observed location")] + public MapboxLocationAccuracyLevel AccuracyLevel; + + // Minimum displacement between location updates in meters. + [Tooltip("Minimum displacement between location updates in meters.")] + public float Displacement; + + /// + /// The fastest rate at which the application will receive location updates, which might be faster than the `Interval`. + /// Unlike `Interval` this parameter is exact. + /// + [Tooltip("The fastest rate at which the application will receive location updates, which might be faster than the `Interval`. Unlike `Interval` this parameter is exact.")] + public long MinimumInterval; + /// + /// Maximum wait time for location updates. If it's at least 2x larger then `Interval`, then location delivery may be delayed and multiple locations can be delivered at once. + /// + [Tooltip("Maximum wait time for location updates. If it's at least 2x larger then `Interval`, then location delivery may be delayed and multiple locations can be delivered at once.")] + public long MaximumInterval; + /// + /// Desired interval for active location updates. + /// + [Tooltip("Desired interval for active location updates.")] + public long Interval; + } + + public enum MapboxLocationAccuracyLevel + { + Passive, + // Low accuracy requirement (typically greater than 500 meters). + Low, + // Medium accuracy requirement (typically between 100 and 500 meters). + Medium, + // High accuracy requirement. + High, + //The highest possible accuracy requirement that uses additional sensors (if possible) to facilitate navigation use case. + Highest + } + + /// + /// Location permissions granted by user to the app. + /// Maps to MBXPermissionStatus from MapboxCommon. + /// + public enum MapboxLocationServiceStatus + { + /// Access to location is not allowed. + Denied = 0, + /// + /// Access to location is allowed. + /// This type of permission is defined for platforms that + /// do not have foreground/background access granularity. + /// + Granted = 1, + /// Access to location is allowed only while an app is in use. + Foreground = 2, + /// Access to location is allowed all the time. + Background = 3 + } + + /// + /// Accuracy authorization granted by user to the app. + /// Maps to MBXAccuracyAuthorization from MapboxCommon. + /// + public enum AccuracyAuthorization + { + /// An app is not authorized to access location. + None = 0, + /// An app is authorized to received as precise as possible location. + Exact = 1, + /// + /// An app is authorized to receive rough location only. + /// Depends on a platform the accuracy is within a city block. + /// + Inexact = 2 + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta new file mode 100644 index 000000000..ec68eb83f --- /dev/null +++ b/Runtime/Mapbox/LocationModule/MapboxLocation/MapboxLocationSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f38b77fb68c43b3a0042227fd6f4696 +timeCreated: 1769266999 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Scripts.meta b/Runtime/Mapbox/LocationModule/Scripts.meta new file mode 100644 index 000000000..5fa752e09 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1a5b7b083ac6940fba41ca91dbbe80e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs similarity index 61% rename from Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs index 4b7989337..59c268867 100644 --- a/Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs @@ -1,5 +1,4 @@ -using System.Collections; -using UnityEngine; +using UnityEngine; namespace Mapbox.LocationModule { @@ -22,27 +21,9 @@ public abstract class AbstractEditorLocationProvider : AbstractLocationProvider #if UNITY_EDITOR protected virtual void Awake() { - _wait = new WaitForSeconds(_updateInterval); - StartCoroutine(QueryLocation()); } #endif - IEnumerator QueryLocation() - { - // HACK: Let others register before we send our first event. - // Often this happens in Start. - yield return new WaitForSeconds(.1f); - while (true) - { - SetLocation(); - if (_autoFireEvent) - { - SendLocation(_currentLocation); - } - yield return _wait; - } - } - // Added to support TouchCamera script. public void SendLocationEvent() diff --git a/Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AbstractEditorLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AbstractEditorLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs similarity index 54% rename from Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs index b7126ec40..2e8f66d85 100644 --- a/Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs @@ -1,9 +1,8 @@ using System; -using UnityEngine; namespace Mapbox.LocationModule { - public abstract class AbstractLocationProvider : MonoBehaviour, ILocationProvider + public abstract class AbstractLocationProvider : ILocationProvider { protected Location _currentLocation; @@ -19,11 +18,27 @@ public Location CurrentLocation } } + /// + /// Has at least one location update been received from the provider? + /// Set once and never reset. + /// + public bool HasReceivedFirstFix { get; protected set; } + public event Action OnLocationUpdated = delegate { }; protected virtual void SendLocation(Location location) { + HasReceivedFirstFix = true; OnLocationUpdated(location); } + + public virtual void Update() + { + + } + + public virtual void OnDestroy() + { + } } } \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AbstractLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AbstractLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs similarity index 92% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs index 67e80925e..6e66b6b5d 100644 --- a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs @@ -1,5 +1,4 @@ using System; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule.AngleSmoothing @@ -7,7 +6,7 @@ namespace Mapbox.LocationModule.AngleSmoothing /// /// Base class for implementing different smoothing strategies /// - public abstract class AngleSmoothingAbstractBase : MonoBehaviour, IAngleSmoothing + public abstract class AngleSmoothingAbstractBase : IAngleSmoothing { @@ -24,7 +23,7 @@ public AngleSmoothingAbstractBase() /// - /// Internal storage for latest 'n' values. Latest value at [0], + /// Internal storage for latest 'n' values. Latest value at [0], /// protected CircularBuffer _angles; diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAbstractBase.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingAverage.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingAverage.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs similarity index 98% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs index 121116375..2b1b3b795 100644 --- a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Linq; namespace Mapbox.LocationModule.AngleSmoothing diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingEMA.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingEMA.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingLowPass.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingLowPass.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/AngleSmoothingNoOp.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/AngleSmoothingNoOp.cs.meta diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs diff --git a/Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/AngleSmoothing/IAngleSmoothing.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/AngleSmoothing/IAngleSmoothing.cs.meta diff --git a/Runtime/Mapbox/LocationModule/CircularBuffer.cs b/Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs similarity index 98% rename from Runtime/Mapbox/LocationModule/CircularBuffer.cs rename to Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs index f93b755d7..0233b6b9e 100644 --- a/Runtime/Mapbox/LocationModule/CircularBuffer.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; -namespace Mapbox.Utils +namespace Mapbox.LocationModule { diff --git a/Runtime/Mapbox/LocationModule/CircularBuffer.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/CircularBuffer.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/CircularBuffer.cs.meta diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs b/Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs rename to Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/EditorLocationProviderLocationLog.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/EditorLocationProviderLocationLog.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs new file mode 100644 index 000000000..a4837432e --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs @@ -0,0 +1,51 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using UnityEngine; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule +{ + public class GetLocationCallbackProxy : AndroidJavaProxy + { + private const string _logPrefix = "MAPBOX_UNITY_SDK: "; + private const string _getLatitude = "getLatitude"; + private const string _getLongitude = "getLongitude"; + + private readonly Action _callback; + + public GetLocationCallbackProxy(Action callback) : base("com.mapbox.common.location.GetLocationCallback") + { + _callback = callback; + } + + [Preserve] + public void run(AndroidJavaObject locationObj) + { + if (locationObj == null) + { + _callback?.Invoke(default(Location), false); + return; + } + + try + { + double lat = locationObj.Call(_getLatitude); + double lon = locationObj.Call(_getLongitude); + + Location location = new Location + { + LatitudeLongitude = new LatitudeLongitude(lat, lon), + TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow) + }; + + _callback?.Invoke(location, true); + } + catch (Exception ex) + { + Debug.LogError(_logPrefix + $"GetLocationCallback error: {ex.Message}"); + _callback?.Invoke(default(Location), false); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta new file mode 100644 index 000000000..91948fbf4 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/GetLocationCallbackProxy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 917962ec991b4b85a23435458f3fd66d +timeCreated: 1770213236 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/ILocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs similarity index 78% rename from Runtime/Mapbox/LocationModule/ILocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs index a3210c807..b63e30f16 100644 --- a/Runtime/Mapbox/LocationModule/ILocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs @@ -9,5 +9,9 @@ public interface ILocationProvider { event Action OnLocationUpdated; Location CurrentLocation { get; } + bool HasReceivedFirstFix { get; } + + void Update(); + void OnDestroy(); } } \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/ILocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/ILocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/ILocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs new file mode 100644 index 000000000..4418d21e2 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs @@ -0,0 +1,16 @@ +using System; +using Mapbox.LocationModule.MapboxLocation; + +namespace Mapbox.LocationModule +{ + public interface IMapboxDeviceLocation + { + event Action LocationUpdated; + event Action AuthorizationChanged; + event Action AccuracyAuthorizationChanged; + event Action AvailabilityChanged; + void Update(); + void OnDestroy(); + void Initialize(); + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta new file mode 100644 index 000000000..31a55c0a7 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/IMapboxDeviceLocation.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7c4b7a0b27194e59a06c8c41b693a3b4 +timeCreated: 1769279814 \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/Location.cs b/Runtime/Mapbox/LocationModule/Scripts/Location.cs similarity index 89% rename from Runtime/Mapbox/LocationModule/Location.cs rename to Runtime/Mapbox/LocationModule/Scripts/Location.cs index c22ba1924..948a0c395 100644 --- a/Runtime/Mapbox/LocationModule/Location.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/Location.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using Mapbox.BaseModule.Data.Vector2d; -using Mapbox.Utils; namespace Mapbox.LocationModule { @@ -94,6 +93,17 @@ public float? SpeedKmPerHour return SpeedMetersPerSecond * 3.6f; } } + + /// + /// Altitude in meters above sea level. 'Null' if not supported by the active location provider. + /// + public double? Altitude; + + /// + /// Bearing in degrees (0-360). Bearing is the horizontal direction of travel of this device, and is not related to the device orientation. 'Null' if not supported by the active location provider. + /// + public float? Bearing; + /// /// Name of the location provider. GPS or network or 'Null' if not supported by the active location provider. /// diff --git a/Runtime/Mapbox/LocationModule/Location.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Location.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Location.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Location.cs.meta diff --git a/Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs similarity index 98% rename from Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs rename to Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs index f8d321c15..08ef16a29 100644 --- a/Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs @@ -2,7 +2,6 @@ using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using Mapbox.BaseModule.Utilities.Attributes; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule diff --git a/Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationArrayEditorLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationArrayEditorLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs new file mode 100644 index 000000000..93286aee4 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections; +using Mapbox.LocationModule.MapboxLocation; +using UnityEngine; + +namespace Mapbox.LocationModule +{ + /// + /// Factory to provide access to various LocationProviders. + /// This is meant to be attached to a game object. + /// + public class LocationProviderFactory : MonoBehaviour + { + public LocationProviderType LocationProviderType; + [NonSerialized] public bool IsLocationProviderReady = false; + public Action OnLocationProviderReady = (f) => { }; + + [SerializeField] + [Tooltip("Mapbox location provider for android and ios")] + private MapboxLocationSettings _mapboxLocationProviderSettings = null; + + [SerializeField] + [Tooltip("Provider using Unity's builtin 'Input.Location' service")] + private UnityLocationProviderSettings _unityLocationProviderSettings; + + private AbstractLocationProvider _customLocationProvider; + + [SerializeField] private string EditorLatitudeLongitude; + + [SerializeField] + bool _dontDestroyOnLoad; + + /// + /// The default location provider. + /// Outside of the editor, this will be a . + /// In the Unity editor, this will be an + /// + public ILocationProvider DefaultLocationProvider { get; set; } + + /// + /// Initialize and inject the DefaultLocationProvider. + /// + public IEnumerator Initialize() + { + if (_dontDestroyOnLoad) + { + DontDestroyOnLoad(gameObject); + } + + if(Application.isEditor || LocationProviderType == LocationProviderType.StaticLocationProvider) + { + DefaultLocationProvider = new StaticLocationProvider(EditorLatitudeLongitude); + } + else if(LocationProviderType == LocationProviderType.MapboxLocationProvider) + { + var mapboxLocationProvider = new MapboxLocationProvider(_mapboxLocationProviderSettings); + mapboxLocationProvider.AvailabilityChanged += b => Debug.Log("MAPBOX_UNITY_SDK: LocationProviderFactory.AvailabilityChanged " + b); + mapboxLocationProvider.AuthorizationChanged += b => Debug.Log("MAPBOX_UNITY_SDK: AuthorizationChanged " + b); + mapboxLocationProvider.AccuracyAuthorizationChanged += b => Debug.Log("MAPBOX_UNITY_SDK: AuthorizationChanged " + b); + DefaultLocationProvider = mapboxLocationProvider; + } + else if(LocationProviderType == LocationProviderType.UnityLocationProvider) + { + DefaultLocationProvider = new UnityLocationProvider(_unityLocationProviderSettings); + } + // else if(LocationProviderType == LocationProviderType.CustomLocationProvider) + // { + // DefaultLocationProvider = new UnityLocationProvider(_unityLocationProviderSettings); + // } + + Debug.Log($"MAPBOX_UNITY_SDK: LocationProviderFactory: Injected Location Provider - {DefaultLocationProvider.GetType()}"); + + if (DefaultLocationProvider.GetType() != typeof(StaticLocationProvider)) + { + if (Input.location.status != LocationServiceStatus.Running) + { + Input.location.Start(); + + while (Input.location.status < LocationServiceStatus.Running) + yield return null; + } + + // Wait for first location update with timeout (10 seconds) + float timeout = 10f; + float elapsed = 0f; + while (!DefaultLocationProvider.HasReceivedFirstFix && + elapsed < timeout) + { + elapsed += UnityEngine.Time.deltaTime; + yield return null; + } + } + + IsLocationProviderReady = true; + OnLocationProviderReady(this); + } + + private void Update() + { + DefaultLocationProvider?.Update(); + } + + private void OnDestroy() + { + DefaultLocationProvider?.OnDestroy(); + } + } + + public enum LocationProviderType + { + [InspectorName("Static Location Provider")] + StaticLocationProvider, + + [InspectorName("Unity Location Provider")] + UnityLocationProvider, + + [InspectorName("Mapbox Location Provider (Experimental)")] + MapboxLocationProvider, + + // [InspectorName("Custom Location Provider")] + // CustomLocationProvider + } +} + + diff --git a/Runtime/Mapbox/LocationModule/LocationProviderFactory.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationProviderFactory.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationProviderFactory.cs.meta diff --git a/Runtime/Mapbox/LocationModule/LocationSmoothing.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationSmoothing.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing.meta diff --git a/Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs b/Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs rename to Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs diff --git a/Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/LocationSmoothing/KalmanFilter.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/LocationSmoothing/KalmanFilter.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Logging.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging.meta diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogAbstractBase.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogAbstractBase.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs similarity index 99% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs index d29e659fb..2a678a794 100644 --- a/Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs @@ -4,7 +4,6 @@ using System.IO; using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule.Logging diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogReader.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogReader.cs.meta diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs similarity index 97% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs index 326eea837..52d4f5559 100644 --- a/Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs @@ -2,7 +2,6 @@ using System.IO; using System.Text; using Mapbox.BaseModule.Utilities; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule.Logging @@ -92,7 +91,6 @@ public void Write(Location location) location.IsLocationUpdated.ToString(), location.IsUserHeadingUpdated.ToString(), location.Provider, - LocationProviderFactory.Instance.DefaultLocationProvider.GetType().Name, DateTime.UtcNow.ToString("yyyyMMdd-HHmmss.fff"), UnixTimestampUtils.From(location.Timestamp).ToString("yyyyMMdd-HHmmss.fff"), string.Format(_invariantCulture, "{0:0.00000000}", location.LatitudeLongitude.Latitude), diff --git a/Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/Logging/LocationLogWriter.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/Logging/LocationLogWriter.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationInfo.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationInfo.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs similarity index 81% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs index ad088e59c..fe38c0108 100644 --- a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs @@ -1,5 +1,3 @@ -using UnityEngine; - namespace Mapbox.LocationModule.UnityLocationWrappers { public interface IMapboxLocationService @@ -8,7 +6,7 @@ public interface IMapboxLocationService bool isEnabledByUser { get; } - LocationServiceStatus status { get; } + UnityEngine.LocationServiceStatus status { get; } IMapboxLocationInfo lastData { get; } diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/IMapboxLocationService.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/IMapboxLocationService.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoMock.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationInfoUnityWrapper.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs similarity index 93% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs index 81d7535a7..db1e3c73f 100644 --- a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using Mapbox.LocationModule.Logging; @@ -75,7 +74,7 @@ protected virtual void Dispose(bool disposeManagedResources) public bool isEnabledByUser { get { return true; } } - public LocationServiceStatus status { get { return _isRunning ? LocationServiceStatus.Running : LocationServiceStatus.Stopped; } } + public UnityEngine.LocationServiceStatus status { get { return _isRunning ? LocationServiceStatus.Running : LocationServiceStatus.Stopped; } } public IMapboxLocationInfo lastData diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceMock.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs similarity index 88% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs index 37f4b3996..e88c2de04 100644 --- a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs +++ b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs @@ -11,7 +11,7 @@ public class MapboxLocationServiceUnityWrapper : IMapboxLocationService public bool isEnabledByUser { get { return Input.location.isEnabledByUser; } } - public LocationServiceStatus status { get { return Input.location.status; } } + public UnityEngine.LocationServiceStatus status { get { return Input.location.status; } } public IMapboxLocationInfo lastData { get { return new MapboxLocationInfoUnityWrapper(Input.location.lastData); } } diff --git a/Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta b/Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta rename to Runtime/Mapbox/LocationModule/Scripts/UnityLocationWrappers/MapboxLocationServiceUnityWrapper.cs.meta diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProvider.cs b/Runtime/Mapbox/LocationModule/StaticLocationProvider.cs similarity index 56% rename from Runtime/Mapbox/LocationModule/EditorLocationProvider.cs rename to Runtime/Mapbox/LocationModule/StaticLocationProvider.cs index 277e00d69..cc1681098 100644 --- a/Runtime/Mapbox/LocationModule/EditorLocationProvider.cs +++ b/Runtime/Mapbox/LocationModule/StaticLocationProvider.cs @@ -2,7 +2,6 @@ using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using Mapbox.BaseModule.Utilities.Attributes; -using Mapbox.Utils; using UnityEngine; namespace Mapbox.LocationModule @@ -11,29 +10,31 @@ namespace Mapbox.LocationModule /// The EditorLocationProvider is responsible for providing mock location and heading data /// for testing purposes in the Unity editor. /// - public class EditorLocationProvider : AbstractEditorLocationProvider + public class StaticLocationProvider : AbstractEditorLocationProvider { - /// - /// The mock "latitude, longitude" location, respresented with a string. - /// You can search for a place using the embedded "Search" button in the inspector. - /// This value can be changed at runtime in the inspector. - /// - [SerializeField] - [Geocode] - private string _latitudeLongitude; private LatitudeLongitude _latLng; + + public StaticLocationProvider(LatitudeLongitude latLng) + { + _latLng = latLng; + _currentLocation = new Location() + { + LatitudeLongitude = _latLng + }; + } -#if UNITY_EDITOR - protected virtual void Start() + public StaticLocationProvider(string latLng) { - base.Awake(); - _latLng = Conversions.StringToLatLon(_latitudeLongitude); + _latLng = Conversions.StringToLatLon(latLng); + _currentLocation = new Location() + { + LatitudeLongitude = _latLng + }; } -#endif protected override void SetLocation() { - _currentLocation.UserHeading = transform.eulerAngles.y; + //_currentLocation.UserHeading = transform.eulerAngles.y; _currentLocation.LatitudeLongitude = _latLng; _currentLocation.Accuracy = _accuracy; _currentLocation.Timestamp = UnixTimestampUtils.To(DateTime.UtcNow); diff --git a/Runtime/Mapbox/LocationModule/EditorLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/StaticLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/EditorLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/StaticLocationProvider.cs.meta diff --git a/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs b/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs new file mode 100644 index 000000000..aeb938413 --- /dev/null +++ b/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs @@ -0,0 +1,227 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities; +using Mapbox.LocationModule.AngleSmoothing; +using Mapbox.LocationModule.UnityLocationWrappers; +using UnityEngine; +using UnityEngine.Android; +using UnityEngine.Scripting; + +namespace Mapbox.LocationModule +{ + [Serializable] + public class UnityLocationProviderSettings + { + /// + /// Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. + /// Values like 5-10 could be used for getting best accuracy. + /// + [SerializeField] + [Tooltip( + "Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. Values like 5-10 could be used for getting best accuracy.")] + public float DesiredAccuracyInMeters = 1.0f; + + /// + /// The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. + /// Higher values like 500 imply less overhead. + /// + [SerializeField] + [Tooltip( + "The minimum distance (measured in meters) a device must move laterally before Input.location property is updated. Higher values like 500 imply less overhead.")] + public float UpdateDistanceInMeters = 0.0f; + + [SerializeField] + [Tooltip( + "The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")] + public long UpdateTimeInMilliSeconds = 500; + + [SerializeField] [Tooltip("Smoothing strategy to be applied to the UserHeading.")] + public AngleSmoothingAbstractBase UserHeadingSmoothing; + + [SerializeField] [Tooltip("Smoothing strategy to applied to the DeviceOrientation.")] + public AngleSmoothingAbstractBase DeviceOrientationSmoothing; + } + + /// + /// The DeviceLocationProvider is responsible for providing real world location and heading data, + /// served directly from native hardware and OS. + /// This relies on Unity's LocationService for location + /// and Compass for heading. + /// + public class UnityLocationProvider : AbstractLocationProvider + { + private IMapboxLocationService _locationService; + private double _lastLocationTimestamp; + private float _lastPollTime; + private float _updateInterval; + + /// list of positions to keep for calculations + private CircularBuffer _lastPositions; + + /// number of last positons to keep + private int _maxLastPositions = 5; + + /// minimum needed distance between oldest and newest position before UserHeading is calculated + private double _minDistanceOldestNewestPosition = 1.5; + + private readonly UnityLocationProviderSettings _unityLocationProviderSettings = new UnityLocationProviderSettings(); + + private const string FineLocation = Permission.FineLocation; + + public UnityLocationProviderSettings UnityLocationProviderSettings => _unityLocationProviderSettings; + + /// + /// Constructor for production use - uses Unity's real location service. + /// + [Preserve] + public UnityLocationProvider(UnityLocationProviderSettings settings) : this(settings, new MapboxLocationServiceUnityWrapper()) + { + } + + /// + /// Constructor for testing - accepts mock location service. + /// Use MapboxLocationServiceMock to replay location logs for testing. + /// + public UnityLocationProvider(UnityLocationProviderSettings settings, IMapboxLocationService locationService) + { + _unityLocationProviderSettings = settings; + _locationService = locationService ?? throw new ArgumentNullException(nameof(locationService)); + //HandlePermission(); + + _currentLocation.Provider = "unity"; + _updateInterval = UnityLocationProviderSettings.UpdateTimeInMilliSeconds < 500 + ? 0.5f + : (float)UnityLocationProviderSettings.UpdateTimeInMilliSeconds / 1000.0f; + + if (null == UnityLocationProviderSettings.UserHeadingSmoothing) + { + UnityLocationProviderSettings.UserHeadingSmoothing = new AngleSmoothingNoOp(); + } + + if (null == UnityLocationProviderSettings.DeviceOrientationSmoothing) + { + UnityLocationProviderSettings.DeviceOrientationSmoothing = new AngleSmoothingNoOp(); + } + + _lastPositions = new CircularBuffer(_maxLastPositions); + } + + public override void Update() + { + if (Time.realtimeSinceStartup - _lastPollTime < _updateInterval) + return; + _lastPollTime = Time.realtimeSinceStartup; + + var lastData = _locationService.lastData; + var timestamp = lastData.timestamp; + + _currentLocation.IsLocationServiceEnabled = + _locationService.status == LocationServiceStatus.Running + || timestamp > _lastLocationTimestamp; + + _currentLocation.IsUserHeadingUpdated = false; + _currentLocation.IsLocationUpdated = false; + + if (!_currentLocation.IsLocationServiceEnabled) + return; + + // device orientation, user heading get calculated below + UnityLocationProviderSettings.DeviceOrientationSmoothing.Add(Input.compass.trueHeading); + _currentLocation.DeviceOrientation = + (float)UnityLocationProviderSettings.DeviceOrientationSmoothing.Calculate(); + + double latitude = lastData.latitude; + double longitude = lastData.longitude; + Vector2d previousLocation = new Vector2d(_currentLocation.LatitudeLongitude.Latitude, _currentLocation.LatitudeLongitude.Longitude); + _currentLocation.LatitudeLongitude = new LatitudeLongitude(latitude, longitude); + + _currentLocation.Accuracy = (float)Math.Floor(lastData.horizontalAccuracy); + // sometimes Unity's timestamp doesn't seem to get updated, or even jump back in time + // do an additional check if location has changed + _currentLocation.IsLocationUpdated = timestamp > _lastLocationTimestamp || !_currentLocation.LatitudeLongitude.Equals(previousLocation); + _currentLocation.Timestamp = timestamp; + _lastLocationTimestamp = timestamp; + + if (_currentLocation.IsLocationUpdated) + { + if (_lastPositions.Count > 0) + { + // only add position if user has moved +1m since we added the previous position to the list + CheapRuler cheapRuler = new CheapRuler(_currentLocation.LatitudeLongitude.Latitude, CheapRulerUnits.Meters); + var p = _currentLocation.LatitudeLongitude; + double distance = cheapRuler.Distance( + new double[] { p.Longitude, p.Latitude }, + new double[] { _lastPositions[0].Longitude, _lastPositions[0].Latitude } + ); + if (distance > 1.0) + { + _lastPositions.Add(_currentLocation.LatitudeLongitude); + } + } + else + { + _lastPositions.Add(_currentLocation.LatitudeLongitude); + } + } + + // if we have enough positions calculate user heading ourselves. + // Unity does not provide bearing based on GPS locations, just + // device orientation based on Compass.Heading. + // nevertheless, use compass for initial UserHeading till we have + // enough values to calculate ourselves. + if (_lastPositions.Count < _maxLastPositions) + { + _currentLocation.UserHeading = _currentLocation.DeviceOrientation; + _currentLocation.IsUserHeadingUpdated = true; + } + else + { + var newestPos = _lastPositions[0]; + var oldestPos = _lastPositions[_maxLastPositions - 1]; + CheapRuler cheapRuler = new CheapRuler(newestPos.Latitude, CheapRulerUnits.Meters); + double distance = cheapRuler.Distance( + new double[] { newestPos.Longitude, newestPos.Latitude }, + new double[] { oldestPos.Longitude, oldestPos.Latitude } + ); + // positions are minimum required distance apart (user is moving), calculate user heading + if (distance >= _minDistanceOldestNewestPosition) + { + float[] lastHeadings = new float[_maxLastPositions - 1]; + + for (int i = 1; i < _maxLastPositions; i++) + { + // atan2 increases angle CCW, flip sign of latDiff to get CW + double latDiff = -(_lastPositions[i].Latitude - _lastPositions[i - 1].Latitude); + double lngDiff = _lastPositions[i].Longitude - _lastPositions[i - 1].Longitude; + // +90.0 to make top (north) 0 degrees + double heading = (Math.Atan2(latDiff, lngDiff) * 180.0 / Math.PI) + 90.0f; + // stay within [0..360] range + if (heading < 0) + { + heading += 360; + } + + if (heading >= 360) + { + heading -= 360; + } + + lastHeadings[i - 1] = (float)heading; + } + + UnityLocationProviderSettings.UserHeadingSmoothing.Add(lastHeadings[0]); + float finalHeading = (float)UnityLocationProviderSettings.UserHeadingSmoothing.Calculate(); + + //fix heading to have 0 for north, 90 for east, 180 for south and 270 for west + finalHeading = finalHeading >= 180.0f ? finalHeading - 180.0f : finalHeading + 180.0f; + + _currentLocation.UserHeading = finalHeading; + _currentLocation.IsUserHeadingUpdated = true; + } + } + + _currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow); + SendLocation(_currentLocation); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs.meta b/Runtime/Mapbox/LocationModule/UnityLocationProvider.cs.meta similarity index 100% rename from Runtime/Mapbox/LocationModule/DeviceLocationProvider.cs.meta rename to Runtime/Mapbox/LocationModule/UnityLocationProvider.cs.meta diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs index b0f1c419a..fdec9d400 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingCacheManagerBehaviour.cs @@ -1,8 +1,7 @@ using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; -using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Unity; -using Mapbox.Example.Scripts.ModuleBehaviours; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.MapDebug.Scripts.Logging; using UnityEngine; diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs index cb34e2d2d..bbe0f2111 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingDataFetchingManagerBehaviour.cs @@ -1,6 +1,6 @@ using System; using Mapbox.BaseModule.Data.DataFetchers; -using Mapbox.Example.Scripts.ModuleBehaviours; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using UnityEngine; namespace Mapbox.MapDebug.Scripts.Logging diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs index 02a958018..0727f9103 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingMapBehaviour.cs @@ -1,13 +1,13 @@ using System; +using System.Collections; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Platform.Cache; using Mapbox.BaseModule.Data.Platform.Cache.SQLiteCache; using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Unity.ModuleBehaviours; using Mapbox.BaseModule.Utilities; -using Mapbox.Example.Scripts.ModuleBehaviours; using Mapbox.Example.Scripts.TileProviderBehaviours; -using Mapbox.ImageModule.Terrain.TerrainStrategies; #if UNITY_RECORDER && UNITY_EDITOR using Mapbox.MapDebug.Sequence; #endif @@ -43,7 +43,7 @@ public virtual void Start() } [ContextMenu("Initialize Map")] - public override void Initialize() + public override IEnumerator Initialize() { _mapLogger = FindObjectOfType(); #if UNITY_RECORDER && UNITY_EDITOR @@ -55,10 +55,11 @@ public override void Initialize() var taskManager = new LoggingTaskManager(); _mapLogger?.AddLogger(taskManager); - UnityContext.Initialize(taskManager); + yield return UnityContext.Initialize(taskManager); var mapboxContext = new MapboxContext(); + yield return mapboxContext.Initialize(); _mapService = GetMapService(mapboxContext, UnityContext); MapServiceReady(_mapService); @@ -97,14 +98,6 @@ private void InitializationCompleted() }); } } - - private void Update() - { - if (InitializationStatus == InitializationStatus.ReadyForUpdates && _mapService.IsReady()) - { - MapboxMap.MapUpdated(); - } - } private void OnValidate() { diff --git a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs index 6691cdcee..fa06b3a6c 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/Logging/LoggingTaskManager.cs @@ -123,7 +123,7 @@ protected override void OnTaskFinished(TaskWrapper task) { base.OnTaskFinished(task); - if (task is MeshGenTaskWrapper meshTask) + if (task is MeshGenTaskWrapper meshTask) { var time = task.FinishedTime - task.StartingTime; _meshGenTaskCount++; diff --git a/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs b/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs index 028d60fee..1a9407a81 100644 --- a/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs +++ b/Runtime/Mapbox/MapDebug/Scripts/TestMap.cs @@ -1,3 +1,4 @@ +using System.Collections; using Mapbox.BaseModule.Data.DataFetchers; using Mapbox.BaseModule.Data.Interfaces; using Mapbox.BaseModule.Data.Platform.Cache; @@ -16,10 +17,11 @@ namespace Mapbox.MapDebug.Scripts { public class TestMap : MonoBehaviour { - public void Start() + public IEnumerator Start() { var mapInfo = new MapInformation("60.1734031,24.9428875"); var mapboxContext = new MapboxContext(); + yield return mapboxContext.Initialize(); var unityContext = new UnityContext(); var taskManager = new TaskManager(); diff --git a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs index 0c0645b9a..9a461ec23 100644 --- a/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/AsyncExtractElevationArray.cs @@ -1,80 +1,126 @@ using System; +using Mapbox.BaseModule.Data.DataFetchers; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; using UnityEngine.Rendering; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.UnityMapService { - public class AsyncExtractElevationArray : IElevationDataExtractionStrategy - { - public void ExtractHeightData(Texture2D texture, Action callback = null) - { - AsyncGPUReadback.Request(texture, 0, (t) => - { - var width = t.width; - var data = t.GetData().ToArray(); - float[] heightData = new float[width * width]; - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + /// + /// Asynchronous terrain-RGB -> float[] elevation decoder using + /// AsyncGPUReadback. The GPU readback callback fires on the main thread with a + /// zero-copy NativeArray<Color32>; the decode loop runs there inside a + /// Burst-compiled via Run(). + /// + public class AsyncExtractElevationArray : IElevationDataExtractionStrategy + { + public void ExtractHeightData(Texture2D texture, Action callback = null) + { + AsyncGPUReadback.Request(texture, 0, (t) => + { + var width = t.width; + var data = t.GetData(); + // Fresh allocation, not pool-rented: this overload hands the buffer to + // external code with no return contract. Pool-renting here would drain + // the pool over the session as callers never call Return. + var heightData = new float[width * width]; + RunDecodeJob(data, width, heightData, out _, out _); + callback?.Invoke(heightData); + }); + } - float r = data[index].g; - float g = data[index].b; - float b = data[index].a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - heightData[(int) (y * width + x)] = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - //678 ==> 012345678 - //345 - //012 - } - } + public void ExtractHeightData(TerrainData terrainData) + { + ExtractHeightData(terrainData.Texture, terrainData); + } - callback?.Invoke(heightData); - }); - } + public void ExtractHeightData(Texture2D texture, TerrainData terrainData) + { + AsyncGPUReadback.Request(texture, 0, (t) => + { + // AsyncGPUReadback fires on the main thread, but possibly after the + // owning TerrainData was disposed (cache eviction during fetch). + // Guard before renting from the pool: assigning to disposed data would + // leak the rented buffer and could corrupt a future Rent if the same + // TerrainData later gets re-used. + if (terrainData == null || terrainData.IsDisposed) + { + return; + } + var width = t.width; + var data = t.GetData(); + var heightData = ElevationArrayPool.Rent(width * width); + RunDecodeJob(data, width, heightData, out var min, out var max); + // Re-check after the (potentially long) decode: dispose may have arrived + // in the interim. Return the buffer rather than assigning into the dead + // data object. + if (terrainData.IsDisposed) + { + ElevationArrayPool.Return(heightData); + return; + } + terrainData.SetElevationValues(heightData, min, max); + }); + } - public void ExtractHeightData(TerrainData terrainData) - { - ExtractHeightData(terrainData.Texture, terrainData); - } + private static void RunDecodeJob(NativeArray data, int width, float[] heightData, out float min, out float max) + { + var length = width * width; + var heightNative = new NativeArray(length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + var minMax = new NativeArray(2, Allocator.TempJob); + new DecodeTerrainColor32Job + { + Data = data, + Width = width, + HeightData = heightNative, + MinMax = minMax + }.Run(); + heightNative.CopyTo(heightData); + min = minMax[0]; + max = minMax[1]; + heightNative.Dispose(); + minMax.Dispose(); + } - public void ExtractHeightData(Texture2D texture, TerrainData terrainData) - { - AsyncGPUReadback.Request(texture, 0, (t) => - { - var min = float.MaxValue; - var max = float.MinValue; - var width = t.width; - var data = t.GetData().ToArray(); - float[] heightData = new float[width * width]; - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + /// + /// Burst-compiled decode for the async path. Mapbox terrain-RGB in the Color32 + /// readback is stored as (A, R, G, B) in (r, g, b, a) positions + /// (historical swizzle preserved from the pre-Burst implementation). Min/max are + /// accumulated inline. + /// + [BurstCompile] + private struct DecodeTerrainColor32Job : IJob + { + [ReadOnly] public NativeArray Data; + public int Width; + public NativeArray HeightData; + public NativeArray MinMax; - float r = data[index].g; - float g = data[index].b; - float b = data[index].a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - var value = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - heightData[(int)(y * width + x)] = value; - if(value < min) min = value; - if(value > max) max = value; - //678 ==> 012345678 - //345 - //012 - } - } - - terrainData.SetElevationValues(heightData, min, max); - }); - } - } -} \ No newline at end of file + public void Execute() + { + var min = float.MaxValue; + var max = float.MinValue; + int idx = 0; + for (int y = 0; y < Width; y++) + { + for (int x = 0; x < Width; x++, idx++) + { + var c = Data[idx]; + float r = c.g; + float g = c.b; + float b = c.a; + var value = -10000f + (r * 65536f + g * 256f + b) * 0.1f; + HeightData[idx] = value; + if (value < min) min = value; + if (value > max) max = value; + } + } + MinMax[0] = min; + MinMax[1] = max; + } + } + } +} diff --git a/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs index 9068febbd..c939137c8 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/ImageSource.cs @@ -32,7 +32,7 @@ protected ImageSource(DataFetchingManager dataFetchingManager, MapboxCacheManage CacheItemDisposed(t); }; } - + public override void LoadTile(CanonicalTileId requestedDataTileId) { LoadTileCore(requestedDataTileId); @@ -127,7 +127,34 @@ public virtual void ClearMemoryCache() { _memoryCache.OnDestroy(); } + + public IEnumerator ReloadTiles() + { + // IMPORTANT: Materialize keys BEFORE starting any coroutines to avoid + // InvalidOperationException: Collection was modified during enumeration. + // RefreshData modifies the cache (Remove/Add) while WaitForAll() would be + // lazily enumerating the live dictionaries if we didn't snapshot them first. + var activeKeys = _memoryCache.GetActiveData.Keys.ToList(); + var fallbackKeys = _memoryCache.GetFallbackData.Keys.ToList(); + + var coroutines = activeKeys.Select(key => RefreshData(key)); + coroutines = coroutines.Concat(fallbackKeys.Select(key => RefreshData(key, data => + { + _memoryCache.MarkFallback(key); + }))); + + yield return coroutines.WaitForAll(); + _memoryCache.ClearInactive(); + } + public override IEnumerator ChangeTilesetId(string tilesetId) + { + _settings.TilesetId = tilesetId; + _tilesetId = _settings.TilesetId; + yield return Initialize(); + yield return ReloadTiles(); + } + public override void OnDestroy() { base.OnDestroy(); @@ -143,60 +170,114 @@ public override void OnDestroy() //COROUTINE METHODS only used in initialization so far #region coroutines - public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileId, Action callback = null) + + /// + /// Shared fetch logic for tile data following the cache-check → file-cache → web-request fallback chain. + /// + /// Tile ID to fetch + /// If true, checks memory cache before starting fetch chain + /// If true, removes existing cache entry before adding new data + /// Callback invoked with result (can be null) + private IEnumerator FetchTileDataCoroutine( + CanonicalTileId requestedDataTileId, + bool checkMemoryCacheFirst, + bool clearExistingCache, + Action callback = null) { T resultData = null; - if (GetInstantData(requestedDataTileId, out resultData)) + + // STEP 1: Check memory cache if requested (LoadTileCoroutine does this, RefreshData doesn't) + if (checkMemoryCacheFirst && GetInstantData(requestedDataTileId, out resultData)) { + callback?.Invoke(resultData); + yield break; } - else if (_waitingList.ContainsKey(requestedDataTileId)) + + // STEP 2: If already being fetched, wait for completion + if (_waitingList.ContainsKey(requestedDataTileId)) { while(_waitingList.ContainsKey(requestedDataTileId)) { yield return null; } GetInstantData(requestedDataTileId, out resultData); + callback?.Invoke(resultData); + yield break; } - else - { - _waitingList[requestedDataTileId] = null; - yield return GetImageCoroutine(requestedDataTileId, _tilesetId, _settings.UseNonReadableTextures, - (data) => - { - resultData = data; - _waitingList.Remove(requestedDataTileId); - - if (resultData != null) - { - data.CacheType = CacheType.FileCache; - _memoryCache.Add(data); - CheckExpiration(data); - } - }); - if (resultData == null) + // STEP 3: Try file cache + _waitingList[requestedDataTileId] = null; + yield return GetImageCoroutine(requestedDataTileId, _tilesetId, _settings.UseNonReadableTextures, + (data) => { - var dataTile = CreateTile(requestedDataTileId, _tilesetId); - _waitingList[requestedDataTileId] = dataTile; - var working = true; - WebRequestData(dataTile, (fetchingResult) => + resultData = data; + _waitingList.Remove(requestedDataTileId); + + if (resultData != null) { - _waitingList.Remove(requestedDataTileId); - if (dataTile.CurrentTileState == TileState.Loaded) - { - resultData = TextureFromWebForCoroutine(dataTile); - } - working = false; - }); - while (working) + data.CacheType = CacheType.FileCache; + + // Clear existing cache if requested (RefreshData does this) + if (clearExistingCache && _memoryCache.Exists(requestedDataTileId)) + _memoryCache.Remove(requestedDataTileId); + + _memoryCache.Add(data); + CheckExpiration(data); + } + }); + + // STEP 4: If not in file cache, fetch from web + if (resultData == null) + { + var dataTile = CreateTile(requestedDataTileId, _tilesetId); + _waitingList[requestedDataTileId] = dataTile; + var working = true; + + WebRequestData(dataTile, (fetchingResult) => + { + _waitingList.Remove(requestedDataTileId); + + if (dataTile.CurrentTileState == TileState.Loaded) { - yield return null; + // Clear existing cache entry if requested (for RefreshData) + if (clearExistingCache && _memoryCache.Exists(requestedDataTileId)) + _memoryCache.Remove(requestedDataTileId); + + // Process web response using shared method + resultData = TextureFromWebForCoroutine(dataTile); } + + working = false; + }); + + while (working) + { + yield return null; } } - + callback?.Invoke(resultData); } + + public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileId, Action callback = null) + { + yield return FetchTileDataCoroutine( + requestedDataTileId, + checkMemoryCacheFirst: true, + clearExistingCache: false, + callback + ); + } + + private IEnumerator RefreshData(CanonicalTileId requestedDataTileId, Action callback = null) + { + yield return FetchTileDataCoroutine( + requestedDataTileId, + checkMemoryCacheFirst: false, + clearExistingCache: true, + callback + ); + } public override IEnumerator LoadTilesCoroutine(IEnumerable retainedTiles, Action> callback = null) { @@ -362,7 +443,7 @@ protected void BackgroundLoad(CanonicalTileId tileId, string tilesetId) }); } - private void CheckExpiration(T cacheItem) + protected virtual void CheckExpiration(T cacheItem) { var dataTask = ReadEtagExpiration(cacheItem, 4); if (dataTask != null) //can be null if sqlite cache isn't available diff --git a/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs index ce44db27f..1f5f30d4b 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/PbfSource.cs @@ -84,7 +84,8 @@ public override void CancelActiveRequests(CanonicalTileId unityTileId) if (_waitingList.ContainsKey(unityTileId)) { var tile = _waitingList[unityTileId]; - CancelFetching(tile, _tilesetId); + if (tile != null) + CancelFetching(tile, _tilesetId); _waitingList.Remove(unityTileId); } } diff --git a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs index 2e275d0f0..c2d1d0ccd 100644 --- a/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs +++ b/Runtime/Mapbox/UnityMapService/DataSources/TerrainSource.cs @@ -15,8 +15,19 @@ public class TerrainSource : ImageSource { protected ImageSourceSettings _settings; private IElevationDataExtractionStrategy _elevationDataExtractionStrategy; - - public TerrainSource(DataFetchingManager dataFetchingManager, MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) + + /// + /// When false, the per-pixel terrain-RGB -> float[] decode pass is skipped + /// entirely — saves ~65K pixel decodes and a ~256KB float[] per tile. The raster + /// Texture is still populated, so shader-elevation rendering keeps working; only + /// CPU consumers (collider builder, QueryElevation APIs, CPU-elevation mode, vector + /// snap-to-terrain modifiers) are disabled. Defaults to true for back-compat; + /// the parent overrides this at Initialize based on + /// its settings. + /// + public bool ExtractCpuElevationData { get; set; } = true; + + public TerrainSource(DataFetchingManager dataFetchingManager, MapboxCacheManager mapboxCacheManager, ImageSourceSettings settings) : base(dataFetchingManager, mapboxCacheManager, settings) { _settings = settings; @@ -26,7 +37,7 @@ public TerrainSource(DataFetchingManager dataFetchingManager, MapboxCacheManager } else { - _elevationDataExtractionStrategy = new SyncExtractElevationArray(); + _elevationDataExtractionStrategy = new SyncExtractElevationArray(); } } @@ -112,7 +123,7 @@ public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileI { terrainData = data; })); - if (terrainData != null) + if (terrainData != null && terrainData.Texture != null && ExtractCpuElevationData) { yield return Runnable.Instance.StartCoroutine(ExtractElevationValues(terrainData)); } @@ -121,26 +132,39 @@ public override IEnumerator LoadTileCoroutine(CanonicalTileId requestedDataTileI protected IEnumerator ExtractElevationValues(TerrainData data) { + // Use the TerrainData overload (sets MinElevation/MaxElevation as part of the + // decode). Previously this path used Action + the 1-arg SetElevationValues, + // which left Min/Max at 0 — meaning RecomputeTerrainBounds never widened the + // shared TerrainInfo for any tile loaded through LoadTileCoroutine. + // + // Subscribe to BOTH ElevationValuesUpdated and the dispose multicast: the async + // GPU readback no-ops on disposed data and never raises ElevationValuesUpdated, + // so without the dispose hook the coroutine would yield forever and pin the + // TerrainData. Rapid pan + cache eviction during initial load is the trigger. var finished = false; - _elevationDataExtractionStrategy.ExtractHeightData(data.Texture, (elevationArray) => - { - data.SetElevationValues(elevationArray); - finished = true; - }); + Action onDone = () => finished = true; + data.ElevationValuesUpdated += onDone; + data.AddDisposeCallback(onDone); + _elevationDataExtractionStrategy.ExtractHeightData(data.Texture, data); while (!finished) yield return null; + data.ElevationValuesUpdated -= onDone; + data.RemoveDisposeCallback(onDone); } protected override void TextureReceivedFromFile(TerrainData cacheItem) { base.TextureReceivedFromFile(cacheItem); - _elevationDataExtractionStrategy.ExtractHeightData(cacheItem); + if (ExtractCpuElevationData) + { + _elevationDataExtractionStrategy.ExtractHeightData(cacheItem); + } } protected override TerrainData TextureReceivedFromWeb(RasterTile tile) { var cacheItem = base.TextureReceivedFromWeb(tile); - if (cacheItem != null) + if (cacheItem != null && ExtractCpuElevationData) { _elevationDataExtractionStrategy.ExtractHeightData(cacheItem); } diff --git a/Runtime/Mapbox/UnityMapService/MapUnityService.cs b/Runtime/Mapbox/UnityMapService/MapUnityService.cs index a7327c719..d744d00bc 100644 --- a/Runtime/Mapbox/UnityMapService/MapUnityService.cs +++ b/Runtime/Mapbox/UnityMapService/MapUnityService.cs @@ -24,7 +24,10 @@ public sealed class MapUnityService : MapService, IUnityMapService private MapboxCacheManager _cacheManager; private DataFetchingManager _fetchingManager; - public override IFileSource FileSource => _fetchingManager; + public override IFileSource FileSource => FetchingManager; + + public MapboxCacheManager CacheManager => _cacheManager; + public DataFetchingManager FetchingManager => _fetchingManager; public MapUnityService( UnityContext unityContext, @@ -63,20 +66,25 @@ public override Source GetNewRasterSource(string name, string tilese public override Source GetTerrainRasterSource(ImageSourceSettings settings) { - var terrainSource = new TerrainSource(_fetchingManager, _cacheManager, settings); + var terrainSource = new TerrainSource(FetchingManager, CacheManager, settings); _dataSources.Add(terrainSource); return terrainSource; } public override Source GetStaticRasterSource(ImageSourceSettings settings) { - var staticRasterSource = new StaticSource(_fetchingManager, _cacheManager, settings); + var staticRasterSource = new StaticSource(FetchingManager, CacheManager, settings); _dataSources.Add(staticRasterSource); return staticRasterSource; } public override Source GetVectorSource(VectorSourceSettings settings) { + if (_dataSources.Any(s => s is VectorSource)) + { + return _dataSources.First(x => x is VectorSource) as Source; + } + var vectorSource = new VectorSource(_fetchingManager, _cacheManager, settings); _dataSources.Add(vectorSource); return vectorSource; @@ -84,19 +92,24 @@ public override Source GetVectorSource(VectorSourceSettings settings public override Source GetBuildingSource(VectorSourceSettings settings) { - var vectorSource = new BuildingSource(_fetchingManager, _cacheManager, settings); + var vectorSource = new BuildingSource(FetchingManager, CacheManager, settings); _dataSources.Add(vectorSource); return vectorSource; } - public MapboxCacheManager GetCacheManager() => _cacheManager; - public DataFetchingManager GetFetchingManager() => _fetchingManager; + public MapboxCacheManager GetCacheManager() => CacheManager; + public DataFetchingManager GetFetchingManager() => FetchingManager; + + public override void ClearCachedData() + { + _cacheManager.ClearCachedData(); + } public override void OnDestroy() { base.OnDestroy(); - _fetchingManager.OnDestroy(); - _cacheManager.OnDestroy(); + FetchingManager.OnDestroy(); + CacheManager.OnDestroy(); } } } diff --git a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs index 76a6fc6a2..5bfc483f5 100644 --- a/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs +++ b/Runtime/Mapbox/UnityMapService/SyncExtractElevationArray.cs @@ -1,81 +1,117 @@ using System; +using Mapbox.BaseModule.Data.DataFetchers; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; using UnityEngine; using TerrainData = Mapbox.BaseModule.Data.DataFetchers.TerrainData; namespace Mapbox.UnityMapService { - public class SyncExtractElevationArray : IElevationDataExtractionStrategy - { - public void ExtractHeightData(Texture2D texture, Action callback) - { - byte[] rgbData = texture.GetRawTextureData(); - var width = texture.width; - float[] heightData = new float[width * width]; + /// + /// Synchronous terrain-RGB -> float[] elevation decoder. Used on platforms that don't + /// support AsyncGPUReadback. The decode loop runs inside a Burst-compiled + /// via Run() — main-thread execution preserves the sync + /// contract, but the hot loop is SIMD-vectorized native code rather than managed. + /// + public class SyncExtractElevationArray : IElevationDataExtractionStrategy + { + public void ExtractHeightData(Texture2D texture, Action callback) + { + var rgbData = texture.GetRawTextureData(); + var width = texture.width; + // Fresh allocation, not pool-rented: this overload hands the buffer to + // external code with no return contract. Pool-renting here would drain + // the pool over the session as callers never call Return. + var heightData = new float[width * width]; + RunDecodeJob(rgbData, width, heightData, out _, out _); + callback?.Invoke(heightData); + } - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + public void ExtractHeightData(TerrainData terrainData) + { + ExtractHeightData(terrainData.Texture, terrainData); + } - float r = rgbData[index * 4 + 1]; - float g = rgbData[index * 4 + 2]; - float b = rgbData[index * 4 + 3]; - //var color = rgbData[index]; - // float r = color.g; - // float g = color.b; - // float b = color.a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - heightData[(int) (y * width + x)] = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - //678 ==> 012345678 - //345 - //012 - } - } - callback?.Invoke(heightData); - } - - public void ExtractHeightData(TerrainData terrainData) - { - ExtractHeightData(terrainData.Texture, terrainData); - } - - public void ExtractHeightData(Texture2D texture, TerrainData terrainData) - { - var min = float.MaxValue; - var max = float.MinValue; - byte[] rgbData = texture.GetRawTextureData(); - var width = texture.width; - float[] heightData = new float[width * width]; + public void ExtractHeightData(Texture2D texture, TerrainData terrainData) + { + // Even the sync path can be called on a disposed TerrainData from cache-fetch + // race paths. Skip + don't rent if already disposed; otherwise we'd + // SetElevationValues on a dead object and leak the rented buffer. + if (terrainData == null || terrainData.IsDisposed) + { + return; + } + var rgbData = texture.GetRawTextureData(); + var width = texture.width; + var heightData = ElevationArrayPool.Rent(width * width); + RunDecodeJob(rgbData, width, heightData, out var min, out var max); + if (terrainData.IsDisposed) + { + ElevationArrayPool.Return(heightData); + return; + } + terrainData.SetElevationValues(heightData, min, max); + } - for (float y = 0; y < width; y++) - { - for (float x = 0; x < width; x++) - { - var xx = (x / width) * width; - var yy = (y / width) * width; - var index = ((int) yy * width) + (int) xx; + // Copies the Burst-decoded result (NativeArray) back into the pooled managed array + // our TerrainData API still uses. The NativeArray lives only for the job's + // lifetime; one temp alloc + memcpy per tile extraction. The job itself computes + // min/max in the same pass so we don't re-iterate on the C# side. + private static void RunDecodeJob(NativeArray rgbData, int width, float[] heightData, out float min, out float max) + { + var length = width * width; + var heightNative = new NativeArray(length, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + var minMax = new NativeArray(2, Allocator.TempJob); + new DecodeTerrainRgbJob + { + RgbData = rgbData, + Width = width, + HeightData = heightNative, + MinMax = minMax + }.Run(); + heightNative.CopyTo(heightData); + min = minMax[0]; + max = minMax[1]; + heightNative.Dispose(); + minMax.Dispose(); + } - float r = rgbData[index * 4 + 1]; - float g = rgbData[index * 4 + 2]; - float b = rgbData[index * 4 + 3]; - //var color = rgbData[index]; - // float r = color.g; - // float g = color.b; - // float b = color.a; - //the formula below is the same as Conversions.GetAbsoluteHeightFromColor but it's inlined for performance - var value = (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); - heightData[(int)(y * width + x)] = value; - if(value < min) min = value; - if(value > max) max = value; - //678 ==> 012345678 - //345 - //012 - } - } - terrainData.SetElevationValues(heightData, min, max); - } - } -} \ No newline at end of file + /// + /// Burst-compiled decode of Mapbox terrain-RGB into linear elevation (meters). + /// Four-byte RGBA layout, formula h = -10000 + (r*65536 + g*256 + b) * 0.1. + /// Min/max are accumulated in the same pass to save a second iteration. + /// + [BurstCompile] + private struct DecodeTerrainRgbJob : IJob + { + [ReadOnly] public NativeArray RgbData; + public int Width; + public NativeArray HeightData; + public NativeArray MinMax; + + public void Execute() + { + var min = float.MaxValue; + var max = float.MinValue; + int idx = 0; + for (int y = 0; y < Width; y++) + { + for (int x = 0; x < Width; x++, idx++) + { + int rgbIdx = idx * 4; + float r = RgbData[rgbIdx + 1]; + float g = RgbData[rgbIdx + 2]; + float b = RgbData[rgbIdx + 3]; + var value = -10000f + (r * 65536f + g * 256f + b) * 0.1f; + HeightData[idx] = value; + if (value < min) min = value; + if (value > max) max = value; + } + } + MinMax[0] = min; + MinMax[1] = max; + } + } + } +} diff --git a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs index 71b8797f2..4b7dffe2d 100644 --- a/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs +++ b/Runtime/Mapbox/UnityMapService/TileProvider/UnityTileProvider.cs @@ -10,164 +10,308 @@ namespace Mapbox.UnityMapService.TileProviders { [Serializable] - public class UnityTileProviderSettings + public class UnityTileProviderSettings : ISerializationCallbackReceiver { + /// + /// Camera used for frustum culling and LOD calculations. + /// Falls back to Camera.main if not set. + /// + [Tooltip("Camera used for frustum culling and LOD calculations. Falls back to Camera.main if not set.")] public Camera Camera; + + /// + /// Tiles below this zoom level are always subdivided without distance checks. + /// Higher values force more subdivisions, producing more tiles to fill the view. + /// Lower values improve performance. Default of 2 works well for most cases. + /// + [Tooltip("Tiles below this zoom are always subdivided regardless of distance. Lower values improve performance. Default 2 works for most cases.")] public float MinimumZoomLevel = 2; + + /// + /// Maximum zoom level the quadtree will subdivide to, clamped by the map's AbsoluteZoom. + /// Higher values produce finer detail near the camera but cost more to render. + /// Lower values improve performance at the expense of close-up detail. + /// Typical range is 14-18 for most applications. + /// + [Tooltip("Maximum tile detail level. Higher = sharper close-up detail but more tiles to render. Typical range 14-18. Default 22.")] public float MaximumZoomLevel = 22; + /// + /// Controls how aggressively tiles subdivide with distance. + /// Higher values produce more tiles with finer detail at distance but cost more to render. + /// Lower values improve performance with coarser detail at distance. + /// Values at or below 0 collapse the split-distance to zero and render only the + /// MinimumZoomLevel tile — kept guarded in OnAfterDeserialize for legacy scenes + /// saved before this field existed (Unity deserializes missing fields as default(T) + /// and does NOT run the C# field initializer in that case). + /// + [Tooltip("Tile detail vs. performance. Higher = more detail at distance, lower = better performance. Below 0.3 may reduce quality. Default 0.6.")] + public float SubdivisionBias = 0.6f; + + /// + /// Expands the 4 side frustum planes outward by this distance, in Unity world units. + /// Pre-loads tiles just outside the screen edges to prevent pop-in during camera pans. + /// Higher values give smoother panning but load more off-screen tiles. + /// Does not affect near/far planes. + /// Tile size varies with mapInformation.Scale: a tile is ~1 unit at scale 1, + /// ~100 units at scale 0.01. Pick a value relative to your scene's current tile size + /// (e.g. ~25% of a tile width for a one-tile-wide buffer). Set to 0 to disable. + /// + [Tooltip("Pre-loads tiles beyond screen edges to reduce pop-in during panning. Value is in Unity world units — scale relative to the tile size at your map's current Scale. Set to 0 to disable.")] + public float FrustumBuffer = 0f; + public UnityTileProviderSettings(Camera cam, float minZoom = 2, float maxZoom = 22) { Camera = cam; MinimumZoomLevel = minZoom; MaximumZoomLevel = maxZoom; } + + public void OnBeforeSerialize() { } + + public void OnAfterDeserialize() + { + // Migrate legacy scenes saved before SubdivisionBias existed (Unity assigns + // default(float) = 0 in that case, collapsing distToSplit and forcing only + // the MinimumZoomLevel tile to render). + if (SubdivisionBias <= 0f) SubdivisionBias = 0.6f; + } + } + + public class UnityTileProvider : TileProvider + { + public UnityTileProviderSettings Settings; + protected int _maxZoom; + protected readonly Plane[] _planes = new Plane[6]; + protected TileNode[] _pool; + protected int _poolCount; + protected readonly Stack _stack; + + // Reusable array for 4 bottom-plane corners to avoid per-frame allocation + protected readonly Vector3[] _corners = new Vector3[4]; + + public UnityTileProvider(UnityTileProviderSettings settings) + { + Settings = settings; + + if (Settings.Camera == null) Settings.Camera = Camera.main; + + if (Settings.MinimumZoomLevel > Settings.MaximumZoomLevel) + { + Debug.LogWarning($"UnityTileProvider: MinimumZoomLevel ({Settings.MinimumZoomLevel}) is greater than MaximumZoomLevel ({Settings.MaximumZoomLevel}). Tiles will be force-split until the absolute zoom cap. Check the Inspector configuration."); + } + + _pool = new TileNode[256]; + _stack = new Stack(256); + } + + protected int AllocNode() + { + if (_poolCount >= _pool.Length) + { + var newPool = new TileNode[_pool.Length * 2]; + Array.Copy(_pool, newPool, _pool.Length); + _pool = newPool; + } + + return _poolCount++; + } + + public override bool GetTileCover(IMapInformation mapInformation, TileCover tileCover) + { + _poolCount = 0; + _stack.Clear(); + tileCover.Tiles.Clear(); + + // Cap at 30: distToSplit uses (1 << (_maxZoom - zoom)), which overflows int + // past 30. Practically unreachable in Mercator (pyramid maxes around z22-24) + // but guards against Inspector misconfig of MaximumZoomLevel. + _maxZoom = (int)Mathf.Min(30f, Mathf.Min(Settings.MaximumZoomLevel, mapInformation.AbsoluteZoom)); + var cam = Settings.Camera; + GeometryUtility.CalculateFrustumPlanes(cam, _planes); + + var camPos = cam.transform.position; + var camForward = cam.transform.forward; + var maxDistSq = cam.farClipPlane * cam.farClipPlane; + + // Expand the 4 side frustum planes (indices 0-3) outward for buffer tile pre-loading. + // Near (4) and far (5) planes are left unchanged. + if (Settings.FrustumBuffer > 0f) + { + for (int i = 0; i < 4; i++) + _planes[i].distance += Settings.FrustumBuffer; + } + var worldCenter = mapInformation.CenterMercator; + var scale = mapInformation.Scale; + + // Bounds extent for frustum culling: derived from observed terrain elevation. + // Min/Max are in meters; divide by scale to get world units. Bottom and top + // follow the observed range directly — clamping the bottom to 0 pessimized + // culling for elevated-only terrain (Himalayan/Andean tiles with Min ≥ +500m) + // by doubling the AABB volume below the actual surface. Negative Min (Death + // Valley, Dead Sea) is handled naturally by letting the bottom go negative. + // Floor on the total height ensures tiles are never zero-height (invisible). + var maxWorld = mapInformation.Terrain.MaxElevation / scale; + var minWorld = mapInformation.Terrain.MinElevation / scale; + var boundsBottom = minWorld; + var boundsHeight = Mathf.Max(maxWorld - boundsBottom, 0.1f); + + // Camera height above the map plane (y=0) + var cameraHeight = camPos.y; + + // zoomSplitDistance: the world-space distance at which a single maxZoom tile + // subtends half the screen height. Derived from the screen-fraction threshold: + // tileSize / dist / (2 * tan(fov/2)) > 0.5 → dist < tileSize / tan(fov/2) + // For a tile at zoom z: distToSplit = (1 << (maxZoom - z)) * zoomSplitDistance + var halfFovTan = Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad); + var maxZoomTileSize = Conversions.TileSizeInUnitySpace(_maxZoom, scale); + var zoomSplitDistance = maxZoomTileSize / halfFovTan; + + // push root tile + var rootIdx = AllocNode(); + _pool[rootIdx].Set(new UnwrappedTileId(0, 0, 0), worldCenter, scale, boundsBottom, boundsHeight); + _stack.Push(rootIdx); + + while (_stack.Count > 0) + { + var idx = _stack.Pop(); + ref var node = ref _pool[idx]; + + if (node.Bounds.SqrDistance(camPos) > maxDistSq) + continue; + + if (!GeometryUtility.TestPlanesAABB(_planes, node.Bounds)) + continue; + + var zoom = node.Id.Z; + + if (zoom >= _maxZoom || !ShouldSplit(zoom, node.Bounds, camPos, camForward, cameraHeight, zoomSplitDistance)) + { + tileCover.Tiles.Add(node.Id); + continue; + } + + // subdivide into 4 children + for (int i = 0; i < 4; i++) + { + var childX = (node.Id.X << 1) + (i & 1); + var childY = (node.Id.Y << 1) + (i >> 1); + var childIdx = AllocNode(); + _pool[childIdx].Set( + new UnwrappedTileId(zoom + 1, childX, childY), + worldCenter, scale, boundsBottom, boundsHeight); + _stack.Push(childIdx); + } + } + + return true; + } + + // Cap for the projDist<=0 "always split" early-exit. Without a cap, a single + // behind-camera corner (admissible after FrustumBuffer expansion) forces 4-way + // subdivision recursively up to _maxZoom — pathologically expensive on tilted + // views where many tiles are partially behind the near plane. 18 is generous + // enough for any reasonable detail level. + private const int AlwaysSplitSafeCap = 18; + + protected bool ShouldSplit(int zoom, Bounds bounds, Vector3 camPos, + Vector3 camForward, float cameraHeight, float zoomSplitDistance) + { + if (zoom < Settings.MinimumZoomLevel) + return true; + + // Find closest forward-projected distance across the 4 bottom-plane corners. + // On a flat map, the closest projected corner is always on the bottom plane. + var min = bounds.min; + var max = bounds.max; + var bottomY = min.y; + _corners[0] = new Vector3(min.x, bottomY, min.z); + _corners[1] = new Vector3(max.x, bottomY, min.z); + _corners[2] = new Vector3(min.x, bottomY, max.z); + _corners[3] = new Vector3(max.x, bottomY, max.z); + + var closestDist = float.MaxValue; + for (int i = 0; i < 4; i++) + { + var offset = _corners[i] - camPos; + // Project onto the ground plane: native SDK overrides this to +cameraHeight, + // which in its tile-coord system stays geometrically consistent. Unity world + // units flip the sign of the vertical dot-product contribution and trigger the + // projDist<=0 early-exit (always-split) for downward-pitched cameras, so we + // zero out the vertical and use ground-plane forward distance instead. + offset.y = 0f; + var projDist = Vector3.Dot(offset, camForward); + if (projDist <= 0f) + { + // Corner behind camera plane — split for better fidelity on the + // partly-visible portion, but cap so we don't recurse all the way + // to _maxZoom (the FrustumBuffer expansion admits behind-camera + // tiles, which would otherwise feed an endless split loop). + return zoom < AlwaysSplitSafeCap; + } + if (projDist < closestDist) + closestDist = projDist; + } + + // Exponential distance threshold: doubles for each zoom level below maxZoom + var distToSplit = (1 << (_maxZoom - zoom)) * zoomSplitDistance * Settings.SubdivisionBias; + + // Acute angle compensation: reduce detail for tiles viewed at grazing angles + distToSplit *= DistToSplitScale(cameraHeight, closestDist); + + return closestDist < distToSplit; + } + + /// + /// Scales the split distance for tiles viewed at acute angles (high pitch / near horizon). + /// When the angle between the camera-to-tile ray and the tile plane drops below 45 degrees, + /// progressively increases the split distance using a geometric series, making it harder + /// for distant foreshortened tiles to subdivide. + /// Ported from native SDK tile_cover.cpp distToSplitScale (lines 263-285). + /// + private static float DistToSplitScale(float dz, float d) + { + const float kAcuteAngleThresholdSin = 0.707f; // sin(45 degrees) + const float kStretchTile = 1.1f; + const float DzFloor = 0.0001f; + + // Camera at (or extremely close to) ground level. Clamping dz to a tiny + // epsilon and then computing r = d/dz makes Pow(1.1, k+1) saturate to Inf + // and the function return ~0, which collapses distToSplit to 0 — the tile + // pins to MinimumZoomLevel instead of subdividing. Street-level walking-sim + // cameras hit this. Return 1.0 (no acute-angle compensation) so the caller's + // normal split distance applies. + if (dz < DzFloor) + return 1f; + + if (d * kAcuteAngleThresholdSin < dz) + return 1f; + + var r = d / dz; + var k = r - 1f / kAcuteAngleThresholdSin; + return r / (1f / kAcuteAngleThresholdSin + + (Mathf.Pow(kStretchTile, k + 1f) - 1f) / (kStretchTile - 1f) - 1f); + } + + protected struct TileNode + { + public UnwrappedTileId Id; + public Bounds Bounds; + + public void Set(UnwrappedTileId id, Vector2d worldCenter, float scale, float boundsBottom, float boundsHeight) + { + Id = id; + var rect = Conversions.TileBoundsInUnitySpace(id, worldCenter, scale); + var sizeX = (float)rect.Size.x; + + // AABB extends from Y=boundsBottom up to boundsBottom+boundsHeight, so the + // center sits halfway between. Lets Death-Valley-like tiles whose surface + // is below Y=0 still pass the frustum test. + var centerY = boundsBottom + boundsHeight * 0.5f; + Bounds = new Bounds( + new Vector3((float)rect.Center.x, centerY, (float)rect.Center.y), + new Vector3(sizeX, boundsHeight, Mathf.Abs(sizeX))); + } + } } - - public class UnityTileProvider : TileProvider - { - public UnityTileProviderSettings Settings; - private float _quadMaxLevel; - private Plane[] _planes = new Plane[6]; - private Stack _stack; - private List _rectpool; - private int _rectIndex = 0; - - public UnityTileProvider(UnityTileProviderSettings settings) - { - Settings = settings; - - if(Settings.Camera == null) Settings.Camera = Camera.main; - - _rectpool = new List(200); - for (int i = 0; i < 200; i++) - { - _rectpool.Add(new UnityRectD()); - } - _stack = new Stack(200); - } - - private UnityRectD GetRectD() - { - if(_rectIndex >= _rectpool.Count) - _rectpool.Add(new UnityRectD()); - var value = _rectpool[_rectIndex]; - _rectIndex++; - return value; - } - - public override bool GetTileCover(IMapInformation mapInformation, TileCover tileCover) - { - _rectIndex = 0; - tileCover.Tiles.Clear(); - _quadMaxLevel = Mathf.Min(Settings.MaximumZoomLevel, mapInformation.AbsoluteZoom); - GeometryUtility.CalculateFrustumPlanes(Settings.Camera, _planes); - var worldBaseBounds = GetRectD(); - worldBaseBounds.Set(new UnwrappedTileId(0, 0, 0), -mapInformation.CenterMercator, mapInformation.Scale, 0); - _stack.Clear(); - _stack.Push(worldBaseBounds); - while (_stack.Count > 0) - { - var tile = _stack.Pop(); - if (!GeometryUtility.TestPlanesAABB(_planes, tile.UnityBounds)) - { - continue; - } - - if (tile.Id.Z == _quadMaxLevel || !ShouldSplit(tile.Id.Z, tile.UnityBounds.size.z, tile.UnityBounds.SqrDistance(Settings.Camera.transform.position))) - { - tileCover.Tiles.Add(tile.Id); - continue; - } - - for (var i = 0; i < 4; i++) - { - var child = tile.Quadrant(GetRectD(), i); - _stack.Push(child); - } - } - - return true; - } - - private bool ShouldSplit(int zoom, float sizeX, float dist) - { - if (zoom < Settings.MinimumZoomLevel) - { - return true; - } - else if (zoom == _quadMaxLevel) - { - return false; - } - - return dist < Mathf.Pow(sizeX , 2); - } - - private struct UnityRectD - { - public UnwrappedTileId Id; - public Bounds UnityBounds; - - private Vector2d _offset; - private float _worldScale; - private float _currentElevationSample; - const int TileSize = 256; - - public void Set(UnwrappedTileId id, Vector2d vector2d, float worldScale, float unityBoundHeight = 1) - { - _currentElevationSample = unityBoundHeight; - _offset = vector2d; - _worldScale = worldScale; - Id = id; - - var min = Conversions.PixelsToMeters( - Id.X * TileSize, - Id.Y * TileSize, - Id.Z); - var max = Conversions.PixelsToMeters( - (Id.X + 1) * TileSize, - (Id.Y + 1) * TileSize, - Id.Z); - - UnityFlatSpaceCalculations( - (float)min.x + vector2d.x, - (float)min.y + vector2d.y, - (float)(max.x - min.x), - worldScale, _currentElevationSample); - } - - private void UnityFlatSpaceCalculations(double boundX, double boundY, double boundZ, float worldScale, float boundHeight) - { - //var boundsTopLeft = bounds.TopLeft; - var topleftX = (float) (boundX / worldScale); - var toplefty = (float) (boundY / worldScale); - var boundsSize = (float)(boundZ / worldScale); - - UnityBounds = new Bounds( - new Vector3(topleftX + boundsSize / 2, boundHeight/2, toplefty - boundsSize / 2), - new Vector3( - boundsSize, - boundHeight, - boundsSize)); - } - - public UnityRectD Quadrant(UnityRectD rectD, int i) - { - var childX = (Id.X << 1) + (i % 2); - var childY = (Id.Y << 1) + (i >> 1); - - rectD.Set(new UnwrappedTileId(Id.Z + 1, childX, childY), _offset, _worldScale, _currentElevationSample); - return rectD; - } - - public UnwrappedTileId QuadrantTileId(int i) - { - var childX = (Id.X << 1) + (i % 2); - var childY = (Id.Y << 1) + (i >> 1); - - return new UnwrappedTileId(Id.Z + 1, childX, childY); - } - } - } -} \ No newline at end of file +} diff --git a/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef b/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef index 245de1840..cd9a2393a 100644 --- a/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef +++ b/Runtime/Mapbox/UnityMapService/UnityMapModule.asmdef @@ -2,7 +2,8 @@ "name": "UnityMapModule", "rootNamespace": "", "references": [ - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:2665a8d13d1b3f18800f46e256720795" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Mapbox/VectorModule/Visualizers.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/BuildingMaterial.mat.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/BuildingMaterial.mat.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingLayer.asset.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingLayer.asset.meta diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset diff --git a/Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset.meta b/Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset.meta similarity index 100% rename from Runtime/Mapbox/VectorModule/Visualizers/Buildings/DefaultBuildingStack.asset.meta rename to Runtime/Mapbox/VectorModule/BasicVisualizers/Buildings/DefaultBuildingStack.asset.meta diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem.meta b/Runtime/Mapbox/VectorModule/ComponentSystem.meta new file mode 100644 index 000000000..ccf3eed8d --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fcaa3c666a2b64f55b6d3ec17506c1de +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta new file mode 100644 index 000000000..e6962d394 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f5ab08040e331423e9e7fd64cbc1e69e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs new file mode 100644 index 000000000..8b745fe0e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.ComponentSystem.Modifiers; +using Mapbox.VectorTile.Contants; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using DecodeGeometry = Mapbox.VectorModule.ComponentSystem.Data.DecodeGeometry; + +namespace Mapbox.VectorModule.ComponentSystem.AreaComponent +{ + public class AreaComponentVisualizer : MapboxComponentVisualizer + { + private AreaComponentSettings _settings; + + public AreaComponentVisualizer(string name, IMapInformation mapInformation, + UnityContext unityContext = null, AreaComponentSettings settings = null) : base(name, mapInformation, + unityContext) + { + _settings = settings ?? new AreaComponentSettings(); + // localPosition (not position) so the y-offset stays in MapRoot-local space + // and composes with any parent transform applied to the map hierarchy. + _layerRootObject.transform.localPosition = new Vector3( + _layerRootObject.transform.localPosition.x, + _settings.GameObjectOffset, + _layerRootObject.transform.localPosition.z); + } + + public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + int classTagIndex = -1; + int typeTagIndex = -1; + for (var i = 0; i < layer.Keys.Count; i++) + { + var key = layer.Keys[i]; + if (key == "class") classTagIndex = i; + if (key == "type") typeTagIndex = i; + } + + var arrayPolygon = new ArrayPolygon(); + var featureCount = layer.FeatureCount(); + var info = new StackMeshInfo(featureCount); + var featureArray = new AreaFeatureUnity[featureCount]; + + for (var i = 0; i < featureCount; i++) + { + //for some reason passing ref values here effects performance quite a bit + var feature = GetFeature(layer, i, ref classTagIndex, ref typeTagIndex); + if (feature == null) continue; + featureArray[i] = feature; + + info.vertexRanges[i] = info.TotalPointCount; + var vertNeeded = feature.VertexData.VertexCount * 5; + info.vertexSize[i] = vertNeeded; + info.TotalPointCount += vertNeeded; + } + + var meshData = new MeshData(info, info.TotalPointCount); + + var poolSize = 10002; + var styleCount = _settings.StyleDictionary.Count; + var triangles = new List(); + var triIndices = new int[styleCount]; + var baseTriIndices = new int[styleCount]; + for (var i = 0; i < styleCount; i++) + { + triangles.Add(new int[poolSize]); + } + + AreaStyle style = null; + var retryCount = 0; + for (int i = 0; i < featureCount; i++) + { + var featureResult = featureArray[i]; + if (featureResult == null) + { + continue; + } + + if (_settings.LayerName == "water") + { + style = _settings.Styles[0]; + } + else if (!_settings.StyleDictionary.TryGetValue(featureResult.Class, out style)) + { + continue; + } + + var si = style.Index; + var featurePreTriIndex = triIndices[si]; + + var vertices = meshData.Vertices.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var normals = meshData.Normals.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var vertexAnchorIndex = meshData.MeshInfo.vertexRanges[i]; + + var triList = triangles[si]; + triIndices[si] = arrayPolygon.Polygonize(vertices, normals, vertexAnchorIndex, triList, triIndices[si], featureResult.VertexData, style.PushUp); + if (triIndices[si] == -1) + { + for (int j = featurePreTriIndex; j < triList.Length; j++) + { + triList[j] = 0; + } + + meshData.Triangles.Add(triList); + meshData.Materials.Add(style.Material); + triangles[si] = new int[poolSize]; + baseTriIndices[si] += triList.Length; + triIndices[si] = 0; + if (retryCount++ < 1) + { + i--; + continue; + } + // Degenerate feature — skip it + retryCount = 0; + continue; + } + retryCount = 0; + + info.triRanges[i] = baseTriIndices[si] + triIndices[si]; + } + + for (var i = 0; i < triangles.Count; i++) + { + var submeshTri = triangles[i]; + meshData.Triangles.Add(submeshTri); + meshData.Materials.Add(_settings.Styles[i].Material); + } + + return meshData; + } + + public override List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + var objectList = new List(); + var entity = _buildingObjectPool.GetObject(); + var mats = new Material[meshData.Triangles.Count]; + for (var i = 0; i < meshData.Triangles.Count; i++) + { + mats[i] = meshData.Materials[i]; + } + + entity.MeshRenderer.materials = mats; + + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); + entity.StackId = 0; + + var mesh = entity.Mesh; + mesh.Clear(); + mesh.SetVertices(meshData.Vertices); + mesh.SetNormals(meshData.Normals); + mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; + + mesh.subMeshCount = meshData.Triangles.Count; + for (var index = 0; index < meshData.Triangles.Count; index++) + { + var submesh = meshData.Triangles[index]; + mesh.SetTriangles(submesh, index); + } + + entity.MeshFilter.sharedMesh = mesh; + objectList.Add(entity.GameObject); + + if (!_results.ContainsKey(tileId)) + _results.Add(tileId, new List()); + _results[tileId].Add(entity); + OnBuildingMeshCreated(tileId, entity.GameObject, meshData); + + return objectList; + } + + private AreaFeatureUnity GetFeature(VectorTileLayer layer, int i, ref int classTagIndex, ref int typeTagIndex) + { + var view = layer.GetViewFor(i); + var layerData = layer.Data.Slice(view.x, view.y); + var featureReader = new PbfReader(layerData); + var feature = new AreaFeatureUnity(); + bool geomTypeSet = false; + while (featureReader.NextByte()) + { + int featureType = featureReader.Tag; + switch ((FeatureType)featureType) + { + case FeatureType.Id: + feature.Id = (ulong)featureReader.Varint(); + break; + case FeatureType.Tags: + var tags = featureReader.GetPackedInt(); + feature.Tags = tags; + break; + case FeatureType.Type: + int geomType = (int)featureReader.Varint(); + feature.GeometryType = (GeomType)geomType; + geomTypeSet = true; + break; + case FeatureType.Geometry: + if (null != feature.GeometryCommands) + { + throw new System.Exception(string.Format("Layer [{0}], feature already has a geometry", + layer.Name)); + } + + //get raw array of commands and coordinates + feature.GeometryCommands = featureReader.GetPackedUnit32(); + break; + default: + featureReader.Skip(); + break; + } + } + + // Discard features with missing/malformed geometry + if (feature.GeometryCommands == null) return null; + + feature.SetProperties(ref layer, ref classTagIndex, ref typeTagIndex); + feature.VertexData = feature.Geometry(new Vector3(layer.Extent, 0, -layer.Extent)); + return feature; + } + + private class AreaFeatureUnity + { + public ulong Id; + public GeomType GeometryType; + public uint[] GeometryCommands; + public FeatureVertexData VertexData; + public int[] Tags; + public string Class; + public string Type; + + public void SetProperties(ref VectorTileLayer layer, ref int classTagIndex, ref int typeTagIndex) + { + if (Tags == null) return; //water has no tags + + var tagCount = Tags.Length; + //some features have odd number of tags + //not sure if it's a bug or data issue + //so -1 here to skip last single tag + for (int i = 0; i < tagCount - 1; i += 2) + { + if (classTagIndex != -1 && Tags[i] == classTagIndex) + { + Class = layer.Values[Tags[i + 1]].ToString(); + } + else if (typeTagIndex != -1 && Tags[i] == typeTagIndex) + { + Type = layer.Values[Tags[i + 1]].ToString(); + } + } + } + + public FeatureVertexData Geometry(Vector3 scale) + { + return DecodeGeometry.GetGeometry(GeometryCommands, scale); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta new file mode 100644 index 000000000..cf938a52e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d4f563de806d4e479e7b7222ecb14a81 +timeCreated: 1765306883 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs new file mode 100644 index 000000000..ee89442f1 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.AreaComponent +{ + [DisplayName("Area Component")] + [CreateAssetMenu(menuName = "Mapbox/Layer Visualizers/Area Component Visualizer")] + public class AreaComponentVisualizerObject : LayerVisualizerConstructor, IPerformanceLayerVisualizer + { + public AreaComponentSettings Settings; + public bool IsActive = true; + + private AreaComponentVisualizer _layerVisualizer; + + public IVectorLayerVisualizer GetLayerVisualizer() + { + return _layerVisualizer; + } + + public override IVectorLayerVisualizer ConstructLayerVisualizer(IMapInformation mapInformation, UnityContext unityContext) + { + Settings.StyleDictionary = new Dictionary(); + var index = 0; + foreach (var areaStyle in Settings.Styles) + { + areaStyle.Index = index; + Settings.StyleDictionary.Add(areaStyle.ClassName, areaStyle); + index++; + } + + _layerVisualizer = new AreaComponentVisualizer(Settings.LayerName, mapInformation, unityContext, Settings); + _layerVisualizer.Active = IsActive; + + _layerVisualizer.OnVectorMeshCreated += OnVectorMeshCreated; + _layerVisualizer.OnVectorMeshDestroyed += OnVectorMeshDestroyed; + + return _layerVisualizer; + } + + public Action OnVectorMeshCreated = list => { }; + public Action OnVectorMeshDestroyed = go => { }; + } + + [Serializable] + public class AreaComponentSettings + { + public string LayerName; + [Tooltip("Vertical offset to be applied to the road component.")] + public float GameObjectOffset; + public Dictionary StyleDictionary = new Dictionary(); + public List Styles; + } + + [Serializable] + public class AreaStyle + { + [NonSerialized] public int Index; + public string ClassName; + public Material Material; + public float PushUp; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta new file mode 100644 index 000000000..4b230537a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/AreaComponent/AreaComponentVisualizerObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6183aeb8971b45778ba9d2c9fc425433 +timeCreated: 1765306532 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta new file mode 100644 index 000000000..dd5e49fa0 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2358e8de79534423d98715fd47be3057 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs new file mode 100644 index 000000000..a2e466a52 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs @@ -0,0 +1,93 @@ +using System; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer +{ + public interface IHeightFilter + { + void FilterHeight(ref float height, ref float minHeight); + } + + [Serializable] + public class BasicExtrusionSettings : IHeightFilter + { + public ExtrusionOptions ExtrusionOptions = new ExtrusionOptions(); + public void FilterHeight(ref float height, ref float minHeight) + { + switch (ExtrusionOptions.extrusionType) + { + case ExtrusionType.AbsoluteHeight: + height = ExtrusionOptions.maximumHeight; + minHeight = 0; + break; + case ExtrusionType.MaxHeight: + height = Mathf.Min(ExtrusionOptions.maximumHeight, height); + minHeight = 0; + break; + case ExtrusionType.MinHeight: + height = Mathf.Max(ExtrusionOptions.minimumHeight, height); + minHeight = 0; + break; + case ExtrusionType.RangeHeight: + height = Mathf.Clamp(height, ExtrusionOptions.minimumHeight, ExtrusionOptions.maximumHeight); + minHeight = 0; + break; + default: + break; + } + + height *= ExtrusionOptions.extrusionScaleFactor; + minHeight *= ExtrusionOptions.extrusionScaleFactor; + } + } + + [Serializable] + public class ChamferSettings : IHeightFilter + { + public float OffsetInMeters; + public ExtrusionOptions ExtrusionOptions = new ExtrusionOptions(); + public void FilterHeight(ref float height, ref float minHeight) + { + switch (ExtrusionOptions.extrusionType) + { + case ExtrusionType.AbsoluteHeight: + height = ExtrusionOptions.maximumHeight; + minHeight = 0; + break; + case ExtrusionType.MaxHeight: + height = Mathf.Min(ExtrusionOptions.maximumHeight, height); + minHeight = 0; + break; + case ExtrusionType.MinHeight: + height = Mathf.Max(ExtrusionOptions.minimumHeight, height); + minHeight = 0; + break; + case ExtrusionType.RangeHeight: + height = Mathf.Clamp(height, ExtrusionOptions.minimumHeight, ExtrusionOptions.maximumHeight); + minHeight = 0; + break; + case ExtrusionType.None: + case ExtrusionType.PropertyHeight: + default: + break; + } + + height *= ExtrusionOptions.extrusionScaleFactor; + minHeight *= ExtrusionOptions.extrusionScaleFactor; + } + } + + [Serializable] + public class ExtrusionOptions + { + public ExtrusionType extrusionType = ExtrusionType.PropertyHeight; + [Tooltip("Property name in feature layer to use for extrusion.")] + public string propertyName = "height"; + public float minimumHeight = 0f; + public float maximumHeight = 1000f; + [Tooltip("Scale factor to multiply the extrusion value of the feature.")] + public float extrusionScaleFactor = 1f; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta new file mode 100644 index 000000000..1f7d9558a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BasicExtrusionSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2b834d2c97f344fba545a6185641da07 +timeCreated: 1765384723 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs new file mode 100644 index 000000000..bdd4edeb5 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.ComponentSystem.Modifiers; +using Mapbox.VectorTile.Contants; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using DecodeGeometry = Mapbox.VectorModule.ComponentSystem.Data.DecodeGeometry; + +namespace Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer +{ + public class BuildingComponentVisualizer : MapboxComponentVisualizer + { + private BuildingComponentSettings _buildingSettings; + + public BuildingComponentVisualizer(string name, IMapInformation mapInformation, + UnityContext unityContext = null, BuildingComponentSettings settings = null) : base(name, mapInformation, + unityContext) + { + _buildingSettings = settings ?? new BuildingComponentSettings(); + } + + public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + int heightTagIndex = -1; + int minHeightTagIndex = -1; + int extrudeTagIndex = -1; + + for (var i = 0; i < layer.Keys.Count; i++) + { + var key = layer.Keys[i]; + if (key == "height") heightTagIndex = i; + if (key == "min_height") minHeightTagIndex = i; + if (key == "extrude") extrudeTagIndex = i; + } + + var arrayPolygon = new ArrayPolygon(); + ArraySnapTerrain arraySnapTerrain = null; + if (_buildingSettings.EnableTerrainSnapping) + { + arraySnapTerrain = new ArraySnapTerrain(_mapInformation); + } + IPerformanceExtrusion arrayHeight; + IHeightFilter settings; + if (_buildingSettings.RoundBuildingCorners) + { + arrayHeight = new ArrayChamferHeight(_buildingSettings.ChamferExtrusionSettings); + settings = _buildingSettings.ChamferExtrusionSettings; + } + else + { + arrayHeight = new ArrayHeight(_buildingSettings.BasicExtrusionSettings); + settings = _buildingSettings.BasicExtrusionSettings; + } + + var featureCount = layer.FeatureCount(); + var info = new StackMeshInfo(featureCount); + + var tileSize = Conversions.TileSizeInUnitySpace(tileId.Z, _mapInformation.Scale); + var featureArray = new BuildingFeatureUnity[featureCount]; + + for (var i = 0; i < featureCount; i++) + { + //for some reason passing ref values here effects performance quite a bit + var feature = GetFeature(layer, i, ref heightTagIndex, ref minHeightTagIndex, ref extrudeTagIndex); + if (feature == null) continue; + featureArray[i] = feature; + + info.vertexRanges[i] = info.TotalPointCount; + var vertNeeded = feature.VertexData.VertexCount * 5; + info.vertexSize[i] = vertNeeded; + info.TotalPointCount += vertNeeded; + } + + var meshData = new MeshData(info, info.TotalPointCount); + + var poolSize = 10002; + var triList = new int[poolSize]; + var triIndex = 0; + var baseTriIndex = 0; + var retryCount = 0; + for (int i = 0; i < featureCount; i++) + { + var featureResult = featureArray[i]; + if (featureResult == null || !featureResult.DoExtrude) + { + //ArrayPool.Shared.Return(featureResult.VertexData.Vertices); + continue; + } + + var featurePreTriIndex = triIndex; + + var vertices = meshData.Vertices.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var normals = meshData.Normals.AsSpan(meshData.MeshInfo.vertexRanges[i], meshData.MeshInfo.vertexSize[i]); + var vertexAnchorIndex = meshData.MeshInfo.vertexRanges[i]; + + triIndex = arrayPolygon.Polygonize(vertices, normals, vertexAnchorIndex, triList, triIndex, featureResult.VertexData); + if (triIndex == -1) + { + for (int j = featurePreTriIndex; j < triList.Length; j++) + { + triList[j] = 0; + } + + meshData.Triangles.Add(triList); + baseTriIndex += triList.Length; + triList = new int[poolSize]; + triIndex = 0; + if (retryCount++ < 1) + { + i--; + continue; + } + // Degenerate feature — skip it + retryCount = 0; + continue; + } + retryCount = 0; + + if (_buildingSettings.EnableTerrainSnapping) + { + arraySnapTerrain.SnapTerrain(vertices, vertices.Length / 5, tileId, tileSize); + } + + var triSpaceRequired = arrayHeight.CalculateTriCountFor(featureResult.VertexData.VertexCount); + if (triSpaceRequired + triIndex >= triList.Length) + { + for (int j = featurePreTriIndex; j < triList.Length; j++) + { + triList[j] = 0; + } + + meshData.Triangles.Add(triList); + baseTriIndex += triList.Length; + triList = new int[triSpaceRequired + triIndex + 3]; + triIndex = 0; + if (retryCount++ < 1) + { + i--; + continue; + } + // Feature requires more space than expected — skip it + retryCount = 0; + continue; + } + else + { + var height = featureResult.Height; + var minHeight = featureResult.MinHeight; + settings.FilterHeight(ref height, ref minHeight); + + triIndex = arrayHeight.Run(vertices, + normals, + vertexAnchorIndex, + triList, + triIndex, + height, + minHeight, + featureResult.VertexData, + tileSize, + _mapInformation.Scale); + } + + info.triRanges[i] = baseTriIndex + triIndex; + //ArrayPool.Shared.Return(featureResult.VertexData.Vertices); + } + + if (triIndex < triList.Length) + { + Array.Resize(ref triList, triIndex); + } + meshData.Triangles.Add(triList); + return meshData; + } + + public override List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + var objectList = new List(); + var entity = _buildingObjectPool.GetObject(); + var mats = new Material[meshData.Triangles.Count]; + for (int i = 0; i < meshData.Triangles.Count; i++) + { + mats[i] = _buildingSettings.Material; + } + + entity.MeshRenderer.materials = mats; + + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); + entity.StackId = 0; + + var mesh = entity.Mesh; + mesh.Clear(); + mesh.SetVertices(meshData.Vertices); + mesh.SetNormals(meshData.Normals); + mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; + + mesh.subMeshCount = meshData.Triangles.Count; + for (var index = 0; index < meshData.Triangles.Count; index++) + { + var submesh = meshData.Triangles[index]; + mesh.SetTriangles(submesh, index); + } + + entity.MeshFilter.sharedMesh = mesh; + objectList.Add(entity.GameObject); + + if (!_results.ContainsKey(tileId)) + _results.Add(tileId, new List()); + _results[tileId].Add(entity); + OnBuildingMeshCreated(tileId, entity.GameObject, meshData); + + return objectList; + } + + private BuildingFeatureUnity GetFeature(VectorTileLayer layer, int i, ref int heightTagIndex, ref int minHeightTagIndex, ref int extrudeTagIndex) + { + var view = layer.GetViewFor(i); + var layerData = layer.Data.Slice(view.x, view.y); + var featureReader = new PbfReader(layerData); + var feature = new BuildingFeatureUnity(); + while (featureReader.NextByte()) + { + int featureType = featureReader.Tag; + switch ((FeatureType)featureType) + { + case FeatureType.Id: + feature.Id = (ulong)featureReader.Varint(); + break; + case FeatureType.Tags: + var tags = featureReader.GetPackedInt(); + feature.Tags = tags; + break; + case FeatureType.Type: + int geomType = (int)featureReader.Varint(); + feature.GeometryType = (GeomType)geomType; + break; + case FeatureType.Geometry: + if (null != feature.GeometryCommands) + { + throw new System.Exception(string.Format("Layer [{0}], feature already has a geometry", + layer.Name)); + } + + //get raw array of commands and coordinates + feature.GeometryCommands = featureReader.GetPackedUnit32(); + break; + default: + featureReader.Skip(); + break; + } + } + + // Discard features with missing/malformed geometry + if (feature.GeometryCommands == null) return null; + + feature.SetProperties(ref layer, ref heightTagIndex, ref minHeightTagIndex, ref extrudeTagIndex); + feature.VertexData = feature.Geometry(new Vector3(layer.Extent, 0, -layer.Extent)); + return feature; + } + + private class BuildingFeatureUnity + { + public ulong Id; + public GeomType GeometryType; + public uint[] GeometryCommands; + public FeatureVertexData VertexData; + public int[] Tags; + public float Height; + public float MinHeight; + public bool DoExtrude; + + public void SetProperties(ref VectorTileLayer layer, ref int heightTagIndex, ref int minHeightTagIndex, ref int extrudeTagIndex) + { + if (Tags == null) return; // Some features have no tags + + var tagCount = Tags.Length; + + for (int i = 0; i < tagCount - 1; i += 2) + { + if (heightTagIndex != -1 && Tags[i] == heightTagIndex) + { + Height = Convert.ToSingle(layer.Values[Tags[i + 1]]); + } + else if (minHeightTagIndex != -1 && Tags[i] == minHeightTagIndex) + { + MinHeight = Convert.ToSingle(layer.Values[Tags[i + 1]]); + } + else if (extrudeTagIndex != -1 && Tags[i] == extrudeTagIndex) + { + DoExtrude = Convert.ToBoolean(layer.Values[Tags[i + 1]]); + } + } + } + + public FeatureVertexData Geometry(Vector3 scale) + { + return DecodeGeometry.GetGeometry(GeometryCommands, scale); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta new file mode 100644 index 000000000..19e823755 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 307aae1ded10457cbaa880223de909fb +timeCreated: 1764598894 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs new file mode 100644 index 000000000..00fcc5c73 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs @@ -0,0 +1,64 @@ +using System; +using System.ComponentModel; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer +{ + [DisplayName("Building Component")] + [CreateAssetMenu(menuName = "Mapbox/Layer Visualizers/Buildings Component Visualizer")] + public class BuildingComponentVisualizerObject : LayerVisualizerConstructor, IPerformanceLayerVisualizer + { + public BuildingComponentSettings Settings; + public bool IsActive = true; + + private BuildingComponentVisualizer _layerVisualizer; + + public IVectorLayerVisualizer GetLayerVisualizer() + { + return _layerVisualizer; + } + + public override IVectorLayerVisualizer ConstructLayerVisualizer(IMapInformation mapInformation, UnityContext unityContext) + { + _layerVisualizer = new BuildingComponentVisualizer("building", mapInformation, unityContext, Settings); + _layerVisualizer.Active = IsActive; + + _layerVisualizer.OnVectorMeshCreated += OnVectorMeshCreated; + _layerVisualizer.OnVectorMeshDestroyed += OnVectorMeshDestroyed; + + return _layerVisualizer; + } + + public Action OnVectorMeshCreated = list => { }; + public Action OnVectorMeshDestroyed = go => { }; + } + + [Serializable] + public class BuildingComponentSettings + { + public bool EnableTerrainSnapping = false; + public bool RoundBuildingCorners = false; + public Material Material; + + public ChamferSettings ChamferExtrusionSettings; + public BasicExtrusionSettings BasicExtrusionSettings; + + public BuildingComponentSettings() + { + EnableTerrainSnapping = false; + ChamferExtrusionSettings = new ChamferSettings() { OffsetInMeters = 1 }; + BasicExtrusionSettings = new BasicExtrusionSettings(); + } + } + + //necessary of editor script stuff + public interface IPerformanceLayerVisualizer + { + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta new file mode 100644 index 000000000..87e5cedd0 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/BuildingComponentVisualizer/BuildingLayerVisualizerObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c6eb41c8ba1c475ab45d048dafafaf77 +timeCreated: 1744829680 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef new file mode 100644 index 000000000..b44f9e65e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef @@ -0,0 +1,17 @@ +{ + "name": "MapboxComponentSystem", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:2400806fb903448e5b7e737b92f1e434" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta new file mode 100644 index 000000000..2aeaacf0b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/ComponentSystem.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5aef8304858a8459cb2204d15bf75ab9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta new file mode 100644 index 000000000..d48e34fe5 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a2be5986f68b541ddaa69836e6c78f13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs new file mode 100644 index 000000000..e481f4b8a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs @@ -0,0 +1,136 @@ +using System.Runtime.CompilerServices; +using Mapbox.VectorTile.Contants; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public static class DecodeGeometry + { + public static FeatureVertexData GetGeometry(uint[] geometryCommands, Vector3 scale) + { + var vertexData = new FeatureVertexData(); + vertexData.Vertices = new Vector3[geometryCommands.Length / 2]; //ArrayPool.Shared.Rent(geometryCommands.Length / 2); + + int geomCmdCnt = geometryCommands.Length; + float cursorX = 0; + float cursorY = 0; + var index = 0; + for (int i = 0; i < geomCmdCnt; i++) + { + uint g = geometryCommands[i]; + Commands cmd = (Commands)(g & 0x7); + uint cmdCount = g >> 3; + + if (cmd == Commands.MoveTo || cmd == Commands.LineTo) + { + for (int j = 0; j < cmdCount; j++) + { + // ZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], out var dx, out var dy); + // cursorX += (dx / scale.x); + // cursorY += (dy / scale.z); + //small micro optimization to push changes into method below to prevent new allocation + ModifyWithZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], ref scale.x, ref scale.z, ref cursorX, ref cursorY); + + i += 2; + //end of part of multipart feature + if (cmd == Commands.MoveTo) + { + vertexData.Submeshes.Add(index); + } + + var pntTmp = new Vector3(cursorX, 0, cursorY); + // { + // x = cursorX * scaleOffset[0] + scaleOffset[2], + // y = 0, + // z = cursorY * scaleOffset[1] - scaleOffset[3] + // }; + vertexData.Vertices[index++] = pntTmp; + } + } + } + + vertexData.Submeshes.Add(index); + vertexData.VertexCount = index; + return vertexData; + } + + public static FeatureVertexData GetGeometry(uint[] geometryCommands, Vector3 scale, Vector4 scaleOffset) + { + var vertexData = new FeatureVertexData(); + vertexData.Vertices = new Vector3[geometryCommands.Length / 2]; //ArrayPool.Shared.Rent(geometryCommands.Length / 2); + + int geomCmdCnt = geometryCommands.Length; + float cursorX = 0; + float cursorY = 0; + var index = 0; + for (int i = 0; i < geomCmdCnt; i++) + { + uint g = geometryCommands[i]; + Commands cmd = (Commands)(g & 0x7); + uint cmdCount = g >> 3; + + if (cmd == Commands.MoveTo || cmd == Commands.LineTo) + { + for (int j = 0; j < cmdCount; j++) + { + // ZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], out var dx, out var dy); + // cursorX += (dx / scale.x); + // cursorY += (dy / scale.z); + //small micro optimization to push changes into method below to prevent new allocation + ModifyWithZigzagDecode(geometryCommands[i + 1], geometryCommands[i + 2], ref scale.x, ref scale.z, ref cursorX, ref cursorY); + i += 2; + //end of part of multipart feature + if (cmd == Commands.MoveTo) + { + vertexData.Submeshes.Add(index); + } + + var pntTmp = new Vector3() + { + x = cursorX * scaleOffset[0] + scaleOffset[2], + y = 0, + z = cursorY * scaleOffset[1] - scaleOffset[3] + }; + vertexData.Vertices[index++] = pntTmp; + } + } + } + + vertexData.Submeshes.Add(index); + vertexData.VertexCount = index; + return vertexData; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ZigzagDecode(uint x, uint y, out float dx, out float dy) + { + dx = (x >> 1) ^ (-(int)(x & 1)); + dy = (y >> 1) ^ (-(int)(y & 1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ModifyWithZigzagDecode(uint x, uint y, ref float scaleX, ref float scaleZ, ref float cursorX, ref float cursorY) + { + cursorX += (((x >> 1) ^ (-(int)(x & 1))) / scaleX); + cursorY += (((y >> 1) ^ (-(int)(y & 1))) / scaleZ); + } + + private static Vector2 ZigzagDecode(uint x, uint y) + { + + //TODO: verify speed improvements using + // new Point2d(){X=x, Y=y} instead of + // new Point3d(x, y); + + //return new Point2d( + // ((x >> 1) ^ (-(x & 1))), + // ((y >> 1) ^ (-(y & 1))) + //); + return new Vector2 + { + x = ((x >> 1) ^ (-(x & 1))), + y = ((y >> 1) ^ (-(y & 1))) + }; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta new file mode 100644 index 000000000..ca769afb4 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/DecodeGeometry.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a0f9f17212154a838a0b291e9bcf6e1f +timeCreated: 1756392359 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs new file mode 100644 index 000000000..583b556dc --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public class FeatureVertexData + { + public Vector3[] Vertices; + public List Submeshes = new List(4); + public int VertexCount; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta new file mode 100644 index 000000000..f160fe1ac --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/FeatureVertexData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 424f4ff2d911468185a807064987b145 +timeCreated: 1756394988 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs new file mode 100644 index 000000000..6b620dc4a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public class MeshData + { + public Vector3[] Vertices; + public Vector3[] Normals; + public Vector2[] UVs; + public List Triangles; + public StackMeshInfo MeshInfo; + public List Materials; + + public MeshData(StackMeshInfo meshInfo, int featureCount) + { + MeshInfo = meshInfo; + Vertices = new Vector3[featureCount]; + Normals = new Vector3[featureCount]; + Triangles = new List(15); + Materials = new List(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta new file mode 100644 index 000000000..6cec4948a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/MeshData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7b5f421d206748e7ac96e8e3978286e0 +timeCreated: 1756394423 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs new file mode 100644 index 000000000..9de01b0e0 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Mapbox.VectorTile.Contants; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct PbfReader + { + /// Tag at current position + public int Tag; + + /// Value at current position + public ulong Value; + //public ulong Pos { get; private set; } + /// Wire type at current position + public WireTypes WireType; + + + private ReadOnlySpan _buffer; + private int _length; + private int _pos; + + + /// + /// PbfReader constructor + /// + /// Byte array containing the raw (already unzipped) tile data + public PbfReader(ReadOnlySpan tileBuffer) + { + _buffer = tileBuffer; + _length = _buffer.Length; + WireType = WireTypes.UNDEFINED; + _pos = 0; + Tag = 0; + Value = 0; + } + + + /// + /// Gets Varint at current position, moves to position after Varint. + /// Throws exception if Varint cannot be decoded + /// + /// Decoded Varint + public long Varint() + { + // convert to base 128 varint + // https://developers.google.com/protocol-buffers/docs/encoding + int shift = 0; + long result = 0; + while (shift < 64) + { + byte b = _buffer[_pos]; + result |= (long)(b & 0x7F) << shift; + _pos++; + if ((b & 0x80) == 0) + { + return result; + } + shift += 7; + } + throw new System.ArgumentException("Invalid varint"); + + } + + + /// + /// Get a view into the buffer. + /// TODO: refactor to return a DataView instead of a byte array + /// + /// Byte array containing the view + public Vector2Int View() + { + // return layer/feature subsections of the main stream + if (Tag == 0) + { + throw new System.Exception("call next() before accessing field value"); + }; + if (WireType != WireTypes.BYTES) + { + throw new System.Exception("not of type string, bytes or message"); + } + + var skipBytes = Varint(); + SkipBytes((int)skipBytes); + + //byte[] buf = new byte[skipBytes]; + //System.Array.Copy(_buffer, (int)_pos - (int)skipBytes, buf, 0, (int)skipBytes); + + return new Vector2Int((int)_pos - (int)skipBytes, (int)skipBytes); + } + + + /// + /// Get repeated `uint`s a current position, move position + /// + /// List of decoded `uint`s + public uint[] GetPackedUnit32() + { + List values = new List(32); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add((uint)Varint()); + } + return values.ToArray(); + } + + public int[] GetPackedInt() + { + List values = new List(32); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add((int)Varint()); + } + return values.ToArray(); + } + + + public List GetPackedSInt32() + { + List values = new List(200); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add(decodeZigZag32((int)Varint())); + } + return values; + } + + + public List GetPackedSInt64() + { + List values = new List(200); + var sizeInByte = Varint(); + var end = _pos + sizeInByte; + while (_pos < end) + { + values.Add(decodeZigZag64((long)Varint())); + } + return values; + } + + + private int decodeZigZag32(int value) + { + return (value >> 1) ^ -(value & 1); + } + + + private long decodeZigZag64(long value) + { + return (value >> 1) ^ -(value & 1); + } + + + /// + /// Get double at current position, move to next position + /// + /// Decoded double + public double GetDouble() + { + double dblVal = System.BitConverter.ToDouble(_buffer.Slice(_pos, 8)); + _pos += 8; + return dblVal; + } + + + /// + /// Get float a current position, move to next position + /// + /// Decoded float + public float GetFloat() + { + float snglVal = System.BitConverter.ToSingle(_buffer.Slice(_pos, 4)); + _pos += 4; + return snglVal; + } + + + /// + /// Get bytes as string + /// + /// Number of bytes to read + /// Decoded string + public string GetString(int length) + { + var str = Encoding.UTF8.GetString(_buffer.Slice(_pos, length)); + _pos += length; + return str; + } + + + /// + /// Move to next byte and set wire type. Throws exeception if tag is out of range + /// + /// Returns false if at end of buffer + public bool NextByte() + { + if (_pos >= _length) + { + return false; + } + // get and process the next byte in the buffer + // return true until end of stream + Value = (ulong)Varint(); + Tag = (int)Value >> 3; + if ( + (Tag == 0 || Tag >= 19000) + && (Tag > 19999 || Tag <= ((1 << 29) - 1)) + ) + { + throw new System.Exception("tag out of range"); + } + WireType = (WireTypes)(Value & 0x07); + return true; + } + + + /// + /// Skip over a Varint + /// + public void SkipVarint() + { + Varint(); + //while (0 == (_buffer[Pos] & 0x80)) + //{ + // Pos++; + // if (Pos >= _length) + // { + // throw new Exception("Truncated message."); + // } + //} + + //if (Pos > _length) + //{ + // throw new Exception("Truncated message."); + //} + } + + + /// + /// Skip bytes + /// + /// Number of bytes to skip + public void SkipBytes(int skip) + { + if (_pos + skip > _length) + { + string msg = string.Format(NumberFormatInfo.InvariantInfo, "[SkipBytes()] skip:{0} pos:{1} len:{2}", skip, _pos, _length); + throw new System.Exception(msg); + } + _pos += skip; + } + + + /// + /// Automatically skip bytes based on wire type + /// + /// New position within the byte array + public int Skip() + { + if (Tag == 0) + { + throw new System.Exception("call next() before calling skip()"); + } + + switch (WireType) + { + case WireTypes.VARINT: + SkipVarint(); + break; + case WireTypes.BYTES: + SkipBytes((int)Varint()); + break; + case WireTypes.FIXED32: + SkipBytes(4); + break; + case WireTypes.FIXED64: + SkipBytes(8); + break; + case WireTypes.UNDEFINED: + throw new System.Exception("undefined wire type"); + default: + throw new System.Exception("unknown wire type"); + } + + return _pos; + } + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta new file mode 100644 index 000000000..9e444a82f --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/PbfReader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 13381fc6c43e4fb8ad898af93ccde49b +timeCreated: 1748540648 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs new file mode 100644 index 000000000..4f836acb1 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs @@ -0,0 +1,21 @@ +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public class StackMeshInfo + { + public int TotalPointCount; + public int TotalTriangleCount; + public int[] vertexRanges; + public int[] vertexSize; + public int[] triRanges; + public int[] triSize; + + public StackMeshInfo(int featureCount) + { + vertexRanges = new int[featureCount]; + vertexSize = new int[featureCount]; + triRanges = new int[featureCount]; + triSize = new int[featureCount]; + TotalPointCount = 0; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta new file mode 100644 index 000000000..e69d1d15f --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/StackMeshInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0e7204faf02446ef9e48cb58c73eebca +timeCreated: 1756394415 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs new file mode 100644 index 000000000..6953d8b6b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs @@ -0,0 +1,19 @@ +using System; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct VectorTile + { + private VectorTileReader _VTR; + + public VectorTile(ReadOnlySpan data) + { + _VTR = new VectorTileReader(ref data); + } + + public bool TryGetLayer(string layerName, out VectorTileLayer layer) + { + return _VTR.TryGetLayer(layerName, out layer); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta new file mode 100644 index 000000000..0ef853d04 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ee2afb0675ad4d1f9a7e4c4c0bdff748 +timeCreated: 1748537811 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs new file mode 100644 index 000000000..04083f04d --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct VectorTileLayer + { + public ReadOnlySpan Data; + + /// + /// Class to access a vector tile layer + /// + public VectorTileLayer(ref ReadOnlySpan data) + { + Data = data; + _FeaturesData = new List(); + Keys = new List(); + Values = new List(); + Name = null; + Version = 0; + Extent = 0; + } + + /// + /// Get number of features. + /// + /// Number of features in this layer + public int FeatureCount() + { + return _FeaturesData.Count; + } + + public Vector2Int GetViewFor(int i) + { + return _FeaturesData[i]; + } + + public void AddFeatureData(Vector2Int data) + { + _FeaturesData.Add(data); + } + + + /// Name of this layer https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L57 + public string Name { get; set; } + + + /// Version of this layer https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L55 + public long Version { get; set; } + + + /// Extent of this layer https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L70 + public float Extent; + + + /// Raw data of the features contained in this layer + private List _FeaturesData { get; set; } + + + /// + /// TODO: switch to 'dynamic' when Unity supports .Net 4.5 + /// Values contained in this layer + /// + public List Values { get; set; } + + + /// + /// Keys contained in this layer + /// + public List Keys { get; set; } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta new file mode 100644 index 000000000..f31863ee7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileLayer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ed0c1e00a70e4aa7af476eee464b39bc +timeCreated: 1748540665 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs new file mode 100644 index 000000000..453b01f57 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Mapbox.VectorTile.Contants; +using UnityEngine; +using ValueType = Mapbox.VectorTile.Contants.ValueType; + +namespace Mapbox.VectorModule.ComponentSystem.Data +{ + public ref struct VectorTileReader + { + private Dictionary _layerViews; + private readonly ReadOnlySpan _data; + + public VectorTileReader(ref ReadOnlySpan data) + { + _data = data; + _layerViews = new Dictionary(); + if (data.IsEmpty) + { + throw new System.Exception("Tile data cannot be empty"); + } + if (data[0] == 0x1f && data[1] == 0x8b) + { + throw new System.Exception("Tile data is zipped"); + } + + layers(data); + } + + private void layers(ReadOnlySpan data) + { + PbfReader tileReader = new PbfReader(data); + while (tileReader.NextByte()) + { + if (tileReader.Tag == (int)TileType.Layers) + { + string name = null; + var view = tileReader.View(); + PbfReader layerView = new PbfReader(_data.Slice(view.x, view.y)); + while (layerView.NextByte()) + { + if (layerView.Tag == (int)LayerType.Name) + { + var strLen = layerView.Varint(); + name = layerView.GetString((int)strLen); + } + else + { + layerView.Skip(); + } + } + if (name == null) + { + Debug.LogWarning("Skipping PBF layer with no name tag"); + continue; + } + _layerViews.Add(name, view); + } + else + { + tileReader.Skip(); + } + } + } + + public bool TryGetLayer(string layerName, out VectorTileLayer layer) + { + if (_layerViews.TryGetValue(layerName, out var layerData)) + { + layer = getLayer(_data.Slice(layerData.x, layerData.y)); + return true; + } + else + { + layer = new VectorTileLayer(); + return false; + } + } + + private VectorTileLayer getLayer(ReadOnlySpan layerData) + { + var layer = new VectorTileLayer(ref layerData); + var layerReader = new PbfReader(layer.Data); + while (layerReader.NextByte()) + { + int layerType = layerReader.Tag; + switch ((LayerType)layerType) + { + case LayerType.Version: + var version = layerReader.Varint(); + layer.Version = version; + break; + case LayerType.Name: + var strLength = layerReader.Varint(); + layer.Name = layerReader.GetString((int)strLength); + break; + case LayerType.Extent: + layer.Extent = (ulong)layerReader.Varint(); + break; + case LayerType.Keys: + var keyBuffer = layerReader.View(); + string key = Encoding.UTF8.GetString(layerData.Slice(keyBuffer.x, keyBuffer.y)); + layer.Keys.Add(key); + break; + case LayerType.Values: + var valueBuffer = layerReader.View(); + var valuesBuffer = layerData.Slice(valueBuffer.x, valueBuffer.y); + var valReader = new PbfReader(valuesBuffer); + while (valReader.NextByte()) + { + switch ((ValueType)valReader.Tag) + { + case ValueType.String: + var stringBuffer = valReader.View(); + string value = Encoding.UTF8.GetString(valuesBuffer.Slice(stringBuffer.x, stringBuffer.y)); + layer.Values.Add(value); + break; + case ValueType.Float: + float snglVal = valReader.GetFloat(); + layer.Values.Add(snglVal); + break; + case ValueType.Double: + double dblVal = valReader.GetDouble(); + layer.Values.Add(dblVal); + break; + case ValueType.Int: + long i64 = valReader.Varint(); + layer.Values.Add(i64); + break; + case ValueType.UInt: + long u64 = valReader.Varint(); + layer.Values.Add(u64); + break; + case ValueType.SInt: + long s64 = valReader.Varint(); + long decoded = (long)((ulong)s64 >> 1) ^ -(s64 & 1); + layer.Values.Add(decoded); + break; + case ValueType.Bool: + long b = valReader.Varint(); + layer.Values.Add(b == 1); + break; + default: + throw new System.Exception(string.Format( + NumberFormatInfo.InvariantInfo + , "NOT IMPLEMENTED valueReader.Tag:{0} valueReader.WireType:{1}" + , valReader.Tag + , valReader.WireType + )); + //uncomment the following lines when not throwing!! + //valReader.Skip(); + //break; + } + } + break; + case LayerType.Features: + layer.AddFeatureData(layerReader.View()); + break; + default: + layerReader.Skip(); + break; + } + } + return layer; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta new file mode 100644 index 000000000..16cf8cac7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Data/VectorTileReader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6e2aca6dc66542d79a0e8659e9b2e54f +timeCreated: 1748540659 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta new file mode 100644 index 000000000..1c184fd7b --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a2aa0b58efc974bf48c85f841a6f7d4d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs new file mode 100644 index 000000000..b54613bc7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs @@ -0,0 +1,63 @@ +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using UnityEditor; + +namespace Mapbox.VectorModule.ComponentSystem.Editor +{ + [CustomEditor(typeof(BuildingComponentVisualizerObject))] + public class BuildingComponentVisualizerObjectEditor : UnityEditor.Editor + { + private SerializedProperty _settingsProp; + + private void OnEnable() + { + + } + + public override void OnInspectorGUI() + { + _settingsProp = serializedObject.FindProperty("Settings"); + serializedObject.Update(); + + EditorGUILayout.LabelField("Building Layer Visualizer", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + if (_settingsProp != null) + { + DrawSettings(_settingsProp); + } + else + { + EditorGUILayout.HelpBox("No _settings field found.", MessageType.Warning); + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawSettings(SerializedProperty settingsProp) + { + var enableTerrainSnapping = settingsProp.FindPropertyRelative("EnableTerrainSnapping"); + var enableChamfer = settingsProp.FindPropertyRelative("RoundBuildingCorners"); + var chamferSettings = settingsProp.FindPropertyRelative("ChamferExtrusionSettings"); + var basicSettings = settingsProp.FindPropertyRelative("BasicExtrusionSettings"); + var material = settingsProp.FindPropertyRelative("Material"); + + EditorGUILayout.PropertyField(material); + EditorGUILayout.PropertyField(enableTerrainSnapping); + EditorGUILayout.Space(); + + enableChamfer.boolValue = EditorGUILayout.Toggle("Enable Rounded Corners", enableChamfer.boolValue); + if (enableChamfer.boolValue) + { + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(chamferSettings, true); + EditorGUI.indentLevel--; + } + else + { + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(basicSettings, true); + EditorGUI.indentLevel--; + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta new file mode 100644 index 000000000..0075bdbd8 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/BuildingLayerVisualizerObjectEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 900de1d5e17a74422be81361b5812d0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs new file mode 100644 index 000000000..a200169b2 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs @@ -0,0 +1,94 @@ +using System; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Editor +{ + [CustomPropertyDrawer(typeof(ExtrusionOptions))] + public class ExtrusionOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + GUILayout.Space(-EditorGUIUtility.singleLineHeight - 2); + + // Find properties + var extrusionTypeProp = property.FindPropertyRelative("extrusionType"); + var propertyNameProp = property.FindPropertyRelative("propertyName"); + var minHeightProp = property.FindPropertyRelative("minimumHeight"); + var maxHeightProp = property.FindPropertyRelative("maximumHeight"); + var scaleFactorProp = property.FindPropertyRelative("extrusionScaleFactor"); + + //part to render extrusion type in dropbox without none setting + { + // Get all enum values except 'None' (index 0) + var enumValues = (ExtrusionType[])Enum.GetValues(typeof(ExtrusionType)); + var enumNames = Enum.GetNames(typeof(ExtrusionType)); + + // Skip "None" + var displayedValues = new ExtrusionType[enumValues.Length - 1]; + var displayedNames = new string[enumValues.Length - 1]; + Array.Copy(enumValues, 1, displayedValues, 0, displayedValues.Length); + Array.Copy(enumNames, 1, displayedNames, 0, displayedNames.Length); + + // Determine current value + var currentType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + // If current value is None, default to first visible option + if (currentType == ExtrusionType.None) + currentType = displayedValues[0]; + + // Draw dropdown without "None" + int selectedIndex = Array.IndexOf(displayedValues, currentType); + int newIndex = EditorGUILayout.Popup("Extrusion Type", Mathf.Max(selectedIndex, 0), displayedNames); + + extrusionTypeProp.enumValueIndex = (int)displayedValues[newIndex]; + } + + // Determine which ExtrusionType is selected + var extrusionType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + DrawReadOnlyProperty(propertyNameProp); + + // Show common field for all types that need propertyName + switch (extrusionType) + { + case ExtrusionType.PropertyHeight: + + break; + + case ExtrusionType.AbsoluteHeight: + EditorGUILayout.PropertyField(maxHeightProp, new GUIContent("Height")); + break; + + case ExtrusionType.MinHeight: + EditorGUILayout.PropertyField(minHeightProp); + break; + + case ExtrusionType.MaxHeight: + EditorGUILayout.PropertyField(maxHeightProp); + break; + case ExtrusionType.RangeHeight: + EditorGUILayout.PropertyField(minHeightProp); + EditorGUILayout.PropertyField(maxHeightProp); + break; + + default: + break; + } + + EditorGUILayout.PropertyField(scaleFactorProp, new GUIContent("Scale Multiplier")); + + void DrawReadOnlyProperty(SerializedProperty prop) + { + GUI.enabled = false; + EditorGUILayout.PropertyField(prop); + GUI.enabled = true; + } + + EditorGUI.EndProperty(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta new file mode 100644 index 000000000..b7f5f4776 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/ExtrusionOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b90e7f40e810f4582a2fae32600824ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef new file mode 100644 index 000000000..c19203adb --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "MapboxComponentSystem.Editor", + "rootNamespace": "", + "references": [ + "GUID:5aef8304858a8459cb2204d15bf75ab9", + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta new file mode 100644 index 000000000..ff1d2ea07 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentSystem.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d9eb5dce6831647d59dc1f65374df0d1 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs new file mode 100644 index 000000000..118092694 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Editor +{ + [CustomEditor(typeof(MapboxComponentsModuleScript))] + public class MapboxComponentsModuleScriptEditor : UnityEditor.Editor + { + private SerializedProperty _layerVisualizersProp; + private GUIStyle _boxStyle; + private List _visualizerTypes; + + private void OnEnable() + { + _layerVisualizersProp = serializedObject.FindProperty("_layerVisualizers"); + + // Find all ScriptableObject types that implement IPerformanceLayerVisualizer + _visualizerTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .Where(t => + typeof(IPerformanceLayerVisualizer).IsAssignableFrom(t) && + !t.IsAbstract) + .ToList(); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + if (_boxStyle == null) + { + _boxStyle = new GUIStyle(GUI.skin.box) + { + padding = new RectOffset(8, 8, 8, 8), + margin = new RectOffset(4, 4, 8, 8) + }; + } + + // --- show info box if list is empty --- + if (_layerVisualizersProp.arraySize == 0) + { + EditorGUILayout.Space(8); + EditorGUILayout.HelpBox("No visualizers added.\nUse the buttons below to add or create a new visualizer.", + MessageType.Info); + EditorGUILayout.Space(8); + } + + for (int i = 0; i < _layerVisualizersProp.arraySize; i++) + { + var element = _layerVisualizersProp.GetArrayElementAtIndex(i); + + EditorGUILayout.BeginVertical(_boxStyle); + EditorGUILayout.BeginHorizontal(); + + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(element, GUIContent.none); + if (EditorGUI.EndChangeCheck()) + serializedObject.ApplyModifiedProperties(); + + if (GUILayout.Button("Remove", GUILayout.Width(70))) + { + _layerVisualizersProp.DeleteArrayElementAtIndex(i); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + break; + } + + EditorGUILayout.EndHorizontal(); + + if (element.objectReferenceValue != null) + { + var editor = CreateEditor(element.objectReferenceValue); + if (editor != null) + { + EditorGUILayout.Space(5); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + editor.OnInspectorGUI(); + EditorGUILayout.EndVertical(); + } + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(4); + } + + EditorGUILayout.Space(8); + + // --- button row --- + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Add Empty Slot", GUILayout.Height(28))) + { + AddEmptySlot(); + } + + if (GUILayout.Button("Add Layer Visualizer", GUILayout.Height(28))) + { + ShowAddVisualizerMenu(); + } + + EditorGUILayout.EndHorizontal(); + + serializedObject.ApplyModifiedProperties(); + } + + private void AddEmptySlot() + { + serializedObject.Update(); + int newIndex = _layerVisualizersProp.arraySize; + _layerVisualizersProp.InsertArrayElementAtIndex(newIndex); + + // ensure it's null, not a duplicate + var el = _layerVisualizersProp.GetArrayElementAtIndex(newIndex); + el.objectReferenceValue = null; + + serializedObject.ApplyModifiedProperties(); + } + + + private void ShowAddVisualizerMenu() + { + GenericMenu menu = new GenericMenu(); + + if (_visualizerTypes.Count == 0) + { + menu.AddDisabledItem(new GUIContent("No visualizer types found")); + } + else + { + foreach (var type in _visualizerTypes) + { + var displayName = type + .GetCustomAttributes(typeof(DisplayNameAttribute), true) + .FirstOrDefault() as DisplayNameAttribute; + string name = (displayName != null) + ? displayName.DisplayName + : ObjectNames.NicifyVariableName(type.Name); + menu.AddItem(new GUIContent(name), false, () => CreateAndAddVisualizer(type)); + } + } + + menu.ShowAsContext(); + } + + private void CreateAndAddVisualizer(Type type) + { + string path = EditorUtility.SaveFilePanelInProject( + "Create Visualizer", + type.Name + ".asset", + "asset", + "Select a location to save the new visualizer asset." + ); + + if (string.IsNullOrEmpty(path)) + return; // cancelled + + var instance = ScriptableObject.CreateInstance(type); + AssetDatabase.CreateAsset(instance, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + serializedObject.Update(); + int newIndex = _layerVisualizersProp.arraySize; + _layerVisualizersProp.InsertArrayElementAtIndex(newIndex); + _layerVisualizersProp.GetArrayElementAtIndex(newIndex).objectReferenceValue = instance; + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta new file mode 100644 index 000000000..15d922bca --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Editor/MapboxComponentsModuleScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 947b59332411d4bb793a6514df9bb18e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs new file mode 100644 index 000000000..ea4cdab0c --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem +{ + public abstract class MapboxComponentVisualizer : VectorLayerVisualizer + { + public int VisualizerId => _id; + private static int _nextId = 0; + private readonly int _id; + + protected ObjectPool _buildingObjectPool; + + public MapboxComponentVisualizer(string name, IMapInformation mapInformation, UnityContext unityContext = null) : base(name, mapInformation, unityContext, null) + { + _id = System.Threading.Interlocked.Increment(ref _nextId); + _buildingObjectPool = new ObjectPool(VectorEntityGenerator, 20); + } + + public virtual MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + return null; + } + + public virtual List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + return null; + } + + public override void UpdateForView(CanonicalTileId canonicalTileId, IMapInformation information) + { + if (_results.TryGetValue(canonicalTileId, out var visuals)) + { + _mapInformation.PositionObjectFor(canonicalTileId, out var position, out var scale); + foreach (var entity in visuals) + { + entity.GameObject.transform.localPosition = position; + entity.GameObject.transform.localScale = scale; + } + } + } + + public override void UnregisterTile(CanonicalTileId tileId) + { + if (_results.ContainsKey(tileId)) + { + foreach (var entity in _results[tileId]) + { + entity.GameObject.SetActive(false); + _buildingObjectPool.Put(entity); + //TODO call finalize for gameobject modifiers here + + OnBuildingMeshDestroyed(tileId, entity.GameObject); + } + + _results.Remove(tileId); + } + + //TODO call unregister for gameobject modifiers here + } + + public override void SetActive(CanonicalTileId canonicalTileId, bool isActive, IMapInformation mapInformation) + { + if (_results.TryGetValue(canonicalTileId, out var visuals)) + { + foreach (var entity in visuals) + { + entity.GameObject.SetActive(isActive); + } + } + } + + public override void ClearCaches() + { + base.ClearCaches(); + } + + private VectorEntity VectorEntityGenerator() + { + var go = new GameObject(); + go.transform.SetParent(_layerRootObject, worldPositionStays: false); + var mf = go.AddComponent(); + mf.sharedMesh = new Mesh(); + var mr = go.AddComponent(); + var tempVectorEntity = new VectorEntity() + { + GameObject = go, + Transform = go.transform, + MeshFilter = mf, + MeshRenderer = mr, + Mesh = mf.sharedMesh + }; + return tempVectorEntity; + } + + public Action OnBuildingMeshCreated = (id, list, info) => { }; + public Action OnBuildingMeshDestroyed = (id, list) => { }; + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta new file mode 100644 index 000000000..347f03101 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2916147689b340d58602c28b004ce5aa +timeCreated: 1744797730 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs new file mode 100644 index 000000000..a3d62c730 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mapbox.BaseModule.Data.DataFetchers; +using Mapbox.BaseModule.Data.Tasks; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem +{ + public class MapboxComponentsModule : VectorLayerModule + { + public MapboxComponentsModule(IMapInformation mapInformation, Source source, + UnityContext unityContext, Dictionary> layerVisualizers, + VectorModuleSettings vectorModuleSettings = null) : base(mapInformation, source, unityContext, + layerVisualizers, vectorModuleSettings) + { + } + + protected override void MeshGeneration(VectorData data, Action callback) + { + var meshTask = new MeshGenTaskWrapper() + { + TileId = data.TileId, + DataAction = () => + { + var result = new BuildingLayerTaskResult(); + result.MeshData = new List(); + var decompressed = Compression.Decompress(data.Data); + var processedTile = new Data.VectorTile(decompressed); + try + { + foreach (var pair in _layerVisualizers) + { + foreach (var layerVisualizer in pair.Value) + { + var visualizer = (MapboxComponentVisualizer)layerVisualizer; + if (visualizer == null || !visualizer.Active || + visualizer.ContainsVisualFor(data.TileId)) + continue; + + if (processedTile.TryGetLayer(visualizer.VectorLayerName, out var layer)) + { + var layerData = visualizer.CreateMesh(data.TileId, layer); + result.MeshData.Add(new BuildingLayerDataResult() + { + LayerName = visualizer.VectorLayerName, + LayerId = visualizer.VisualizerId, + MeshData = layerData + }); + } + } + } + } + catch (Exception e) + { + result.ResultType = TaskResultType.MeshGenerationFailure; + result.AddException(e); + return result; + } + + result.ResultType = TaskResultType.Success; + return result; + }, + DataCompleted = (task, taskResult) => //task may be null + { + if (!_isActive) + return; + + _activeTasks.Remove(data.TileId); + + if (taskResult.ResultType == TaskResultType.Cancelled || (task != null && task.IsCanceled)) + { + var failResult = new MeshGenerationTaskResult(TaskResultType.Cancelled); + callback(failResult); + return; + } + + if (taskResult.ResultType == TaskResultType.MeshGenerationFailure) + { + var failResult = new MeshGenerationTaskResult(taskResult.ResultType); + foreach (var e in taskResult.GetExceptions()) + { + failResult.AddException(e); + } + + //Debug.Log(string.Format("{0} mesh gen exception: {1}", data.TileId, task.Exception.Message)); + failResult.AddException(new Exception(string.Format("{0} mesh gen exception: {1}", data.TileId, + taskResult.ExceptionsAsString))); + callback(failResult); + return; + } + + var resultGameObjects = new List(); + foreach (var layerData in taskResult.MeshData) + { + // foreach (var vectorLayerVisualizers in _layerVisualizers + // .Where(x => x.Value is MapboxComponentVisualizer && x.Key == layerData.LayerName) + // .Select(x => x.Value)) + IVectorLayerVisualizer visualizer = null; + var visualizers = _layerVisualizers[layerData.LayerName]; + for (int i = 0; i < visualizers.Count; i++) + { + if (((MapboxComponentVisualizer)visualizers[i]).VisualizerId == layerData.LayerId) + { + visualizer = visualizers[i]; + break; + } + } + if (visualizer == null) + continue; + + var layerVisualizer = (MapboxComponentVisualizer)visualizer; + if (layerData.MeshData == null) + continue; + var layerGameObjects = layerVisualizer.CreateGo(data.TileId, layerData.MeshData); + foreach (var gameObject in layerGameObjects) + { + gameObject.SetActive(false); + resultGameObjects.Add(gameObject); + } + + } + + callback(new MeshGenerationTaskResult(TaskResultType.Success, resultGameObjects)); + } + }; + if (!_activeTasks.ContainsKey(data.TileId)) _activeTasks.Add(data.TileId, new List()); + _activeTasks[data.TileId].Add(meshTask); + _unityContext.TaskManager.AddTask(meshTask, 0); + } + + public class BuildingLayerTaskResult : MeshGenTaskWrapperResult + { + public List MeshData; + } + + public class BuildingLayerDataResult + { + public string LayerName; + public MeshData MeshData; + public int LayerId; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta new file mode 100644 index 000000000..deb070266 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModule.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa0b49cb2cc94dc6a1c07f652c4bf589 +timeCreated: 1758131442 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs new file mode 100644 index 000000000..367214ba7 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem +{ + public class MapboxComponentsModuleScript : ModuleConstructorScript + { + [SerializeField] private List _layerVisualizers; + public override ILayerModule ModuleImplementation { get; protected set; } + + public void Start() + { + + } + + protected VectorLayerModule GetVectorLayerModule(IMapInformation mapInformation, UnityContext unityContext, MapService service, Dictionary> dictionary) + { + var tilesetId = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8).Id; + var settings = new VectorModuleSettings() + { + DataSettings = new VectorSourceSettings() + { + TilesetId = tilesetId, + CacheSize = 100, + ClampDataLevelToMax = 15 + } + }; + return new MapboxComponentsModule(mapInformation, service.GetVectorSource(settings.DataSettings), unityContext, dictionary, settings); + } + + public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, UnityContext unityContext) + { + var dictionary = new Dictionary>(); + foreach (var visualizerObject in _layerVisualizers) + { + if(visualizerObject == null) continue; + var visualizer = visualizerObject.ConstructLayerVisualizer(mapInformation, unityContext); + if(!dictionary.ContainsKey(visualizer.VectorLayerName)) + dictionary.Add(visualizer.VectorLayerName, new List()); + dictionary[visualizer.VectorLayerName].Add(visualizer); + } + ModuleImplementation = GetVectorLayerModule(mapInformation, unityContext, service, dictionary); + return ModuleImplementation; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta new file mode 100644 index 000000000..f59116d81 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/MapboxComponentsModuleScript.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e13965577acb456182a04c56586cb36e +timeCreated: 1758132173 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta new file mode 100644 index 000000000..47d0f134e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff49ecc199aa749ac8853d418dd79076 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs new file mode 100644 index 000000000..8f4e5b2e9 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs @@ -0,0 +1,296 @@ +using System; +using System.Runtime.CompilerServices; +using Mapbox.BaseModule.Map; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public interface IPerformanceExtrusion + { + int CalculateTriCountFor(int totalPointCount); + + int Run(Span vertices, Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + float height, + float minHeight, + FeatureVertexData vertexData, + float tileSizeX, + float scale); + } + + public class ArrayChamferHeight : IPerformanceExtrusion + { + private ChamferSettings _settings; + + private static readonly Vector3 Up = Vector3.up; + + public ArrayChamferHeight(ChamferSettings settings) + { + _settings = settings; + } + + public int Run(Span vertices, + Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + float height, + float minHeight, + FeatureVertexData vertexData, + float tileSizeX, + float scale) + { + if (vertexData == null || vertexData.Submeshes.Count < 1) + return triIndex; + + var scaledOffset = (_settings.OffsetInMeters / tileSizeX) / scale; + + height = (height / scale) / tileSizeX; + minHeight = (minHeight > 0) + ? (minHeight / scale) / tileSizeX + : 0; + + triIndex = Chamfer(vertices, normals, vertexData, triList, triIndex, minHeight, height, scaledOffset, vertexAnchorIndex); + return triIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 NormalizeXZ(Vector3 v) + { + float invLen = 1.0f / Mathf.Sqrt(v.x * v.x + v.z * v.z + 1e-12f); + v.x *= invLen; + v.z *= invLen; + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 PerpXZ(Vector3 v) => new Vector3(-v.z, 0f, v.x); + + /// + /// |-x1-|-y1-|-z1-|-x2-|-y2-|-z2-| + /// x1: original polygon vertices (will get changed) + /// y1, z1: original submesh/hole vertices (will get changed) + /// x2,y2,z2: chamfer and side wall vertices + /// originalPolygonPointIndexer: index of the point we are currently processing. whole loop works on the original + /// polygon points so this is the anchor point. it should move in (x1,z1) range. + /// addedPointCount: pointer for x2|y2|z2 section start. pointer for where the second ring of submesh starts. + /// + /// + /// + /// + /// + /// + /// + private int Chamfer( + Span vertices, + Span normals, + FeatureVertexData vertexData, + int[] trilist, + int triIndex, + float minHeight, + float height, + float scaledOffset, + int startIndex) + { + int polyPointCount = vertexData.VertexCount; + int originalPolygonPointIndexer = 0; + int addedPointCount = 0; + const int vertPerStep = 4; + + // Local caches + Vector3 v1, v2, n1, n2, pij1, pij2, pjk1, pjk2, poi; + Vector3 prev, curr, next; + var verts = vertexData.Vertices; + var submeshes = vertexData.Submeshes; + + for (int i = 1; i < submeshes.Count; i++) + { + int start = submeshes[i - 1]; + int end = submeshes[i]; + int count = end - start; + var subSpan = verts.AsSpan(start, count); + + int secondRingStartIndex = polyPointCount + addedPointCount; + + for (int j = 0; j < count; j++) + { + int jPrev = (j == 0) ? count - 1 : j - 1; + int jNext = (j == count - 1) ? 0 : j + 1; + + prev = subSpan[jPrev]; + curr = subSpan[j]; + next = subSpan[jNext]; + + // ---- precompute normalized edge directions ---- + v1.x = curr.x - prev.x; + v1.y = 0f; + v1.z = curr.z - prev.z; + v1 = NormalizeXZ(v1); + n1 = PerpXZ(v1); + v1.x *= scaledOffset; + v1.z *= scaledOffset; + + pij1.x = prev.x - n1.x * scaledOffset; + pij1.y = 0f; + pij1.z = prev.z - n1.z * scaledOffset; + + pij2.x = curr.x - n1.x * scaledOffset; + pij2.y = 0f; + pij2.z = curr.z - n1.z * scaledOffset; + + v2.x = next.x - curr.x; + v2.y = 0f; + v2.z = next.z - curr.z; + v2 = NormalizeXZ(v2); + n2 = PerpXZ(v2); + v2.x *= scaledOffset; + v2.z *= scaledOffset; + + pjk1.x = curr.x - n2.x * scaledOffset; + pjk1.y = 0f; + pjk1.z = curr.z - n2.z * scaledOffset; + + pjk2.x = next.x - n2.x * scaledOffset; + pjk2.y = 0f; + pjk2.z = next.z - n2.z * scaledOffset; + + // ---- intersection ---- + if (pij2.x != pjk1.x || pij2.z != pjk1.z) + FindIntersection(pij1, pij2, pjk1, pjk2, out poi); + else + poi = pij2; + + int vertBase = secondRingStartIndex + vertPerStep * j; + int curIdx = originalPolygonPointIndexer + j; + + Vector3 vertCurr = vertices[curIdx]; + float yTop = vertCurr.y + height; + float yMin = vertCurr.y + minHeight; + + // write vertices (inline, minimal constructors) + vertices[vertBase] = new Vector3(vertCurr.x - v1.x, yTop, vertCurr.z - v1.z); + vertices[vertBase + 1] = new Vector3(vertCurr.x + v2.x, yTop, vertCurr.z + v2.z); + vertices[vertBase + 2] = new Vector3(vertCurr.x - v1.x, yMin, vertCurr.z - v1.z); + vertices[vertBase + 3] = new Vector3(vertCurr.x + v2.x, yMin, vertCurr.z + v2.z); + + // move original vertex + vertices[curIdx] = new Vector3(poi.x, yTop + scaledOffset, poi.z); + + // normals + normals[curIdx] = Up; + normals[vertBase] = n1; + normals[vertBase + 1] = n2; + normals[vertBase + 2] = n1; + normals[vertBase + 3] = n2; + + // ---- triangles ---- + int si = startIndex; + int baseA = si + curIdx; + int baseB = si + vertBase; + + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseB; + trilist[triIndex++] = baseB + 1; + + bool notLast = j < count - 1; + if (notLast) + { + int nextTri = j + 1; + int baseNext = si + originalPolygonPointIndexer + nextTri; + int baseBnext = si + secondRingStartIndex + vertPerStep * nextTri; + + trilist[triIndex++] = baseNext; + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseBnext; + + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseBnext; + + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseB + 3; + trilist[triIndex++] = baseBnext; + + trilist[triIndex++] = baseBnext; + trilist[triIndex++] = baseB + 3; + trilist[triIndex++] = baseBnext + 2; + } + else + { + int baseFirst = si + originalPolygonPointIndexer; + int baseBfirst = si + secondRingStartIndex; + + trilist[triIndex++] = baseFirst; + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseBfirst; + + trilist[triIndex++] = baseBfirst; + trilist[triIndex++] = baseA; + trilist[triIndex++] = baseB + 1; + + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseB + 3; + trilist[triIndex++] = baseBfirst + 2; + + trilist[triIndex++] = baseBfirst; + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseBfirst + 2; + } + + trilist[triIndex++] = si + vertBase; + trilist[triIndex++] = si + vertBase + 2; + trilist[triIndex++] = si + vertBase + 3; + + trilist[triIndex++] = si + vertBase + 1; + trilist[triIndex++] = si + vertBase; + trilist[triIndex++] = si + vertBase + 3; + } + + originalPolygonPointIndexer += subSpan.Length; + addedPointCount += vertPerStep * subSpan.Length; + } + + return triIndex; + } + + private static void FindIntersection( + Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, + out Vector3 intersection) + { + float dx12 = p2.x - p1.x; + float dz12 = p2.z - p1.z; + float dx34 = p4.x - p3.x; + float dz34 = p4.z - p3.z; + + float denominator = dz12 * dx34 - dx12 * dz34; + + // Check for parallel or nearly parallel lines + if (Mathf.Abs(denominator) < 1e-8f) + { + intersection = new Vector3(float.NaN, 0f, float.NaN); + return; + } + + float dx13 = p1.x - p3.x; + float dz13 = p1.z - p3.z; + + float t1 = (dx13 * dz34 + (p3.z - p1.z) * dx34) / denominator; + + intersection = new Vector3( + p1.x + dx12 * t1, + 0f, + p1.z + dz12 * t1 + ); + } + + public int CalculateTriCountFor(int totalPointCount) + { + return totalPointCount * 21; //7 tris , 3 int per + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta new file mode 100644 index 000000000..0a3bb5737 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayChamferHeight.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 454b9196af594778a01eec9395e81223 +timeCreated: 1744834620 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs new file mode 100644 index 000000000..deae1988e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs @@ -0,0 +1,203 @@ +using System; +using System.Runtime.CompilerServices; +using Mapbox.BaseModule.Map; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public class ArrayHeight : IPerformanceExtrusion + { + private readonly BasicExtrusionSettings _settings; + + public ArrayHeight(BasicExtrusionSettings settings) + { + _settings = settings; + } + + public int Run( + Span vertices, + Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + float height, + float minHeight, + FeatureVertexData vertexData, + float tileSizeX, + float scale) + { + if (vertexData == null || vertexData.Submeshes.Count < 1) + return triIndex; + + var startIndex = vertexAnchorIndex; + + // Convert heights to local (tile) units once + height = (height / scale) / tileSizeX; + float minH = (minHeight > 0) + ? (minHeight / scale) / tileSizeX + : 0; + + float wallHeight = height - minH; + + // Raise the roof (top ring/verts in-span) + GenerateRoofMesh(vertices, height); + + // Build walls + triIndex = GenerateWallMesh(vertices, normals, vertexData, triList, triIndex, startIndex, wallHeight, height); + + return triIndex; + } + + public void GenerateRoofMesh(Span vertices, float maxHeight) + { + // Mutate by ref to avoid creating new Vector3 + for (int i = 0; i < vertices.Length; i++) + { + ref Vector3 v = ref vertices[i]; + v.y = maxHeight; + } + } + + private int GenerateWallMesh( + Span vertices, + Span normals, + FeatureVertexData vertexData, + int[] trilist, + int triIndex, + int startIndex, + float wallHeight, + float roofHeight) + { + // Aliases for quicker access + var verts = vertexData.Vertices; + var submeshes = vertexData.Submeshes; + + int topPolygonVertexCount = vertexData.VertexCount; + + // For each ring (outer + holes) + // We assume wall-vertex area starts after top polygon: each edge adds 4 vertices + for (int i = 1; i < submeshes.Count; i++) + { + int start = submeshes[i - 1]; + int end = submeshes[i]; + int count = end - start; + + var subSpan = verts.AsSpan(start, count); + int ringBase = topPolygonVertexCount + (start * 4); // base in the wall region for this submesh + var vertSpan = vertices.Slice(ringBase, count * 4); + var normSpan = normals.Slice(ringBase, count * 4); + + // Walk edges + for (int j = 0; j < count; j++) + { + int jNext = (j == count - 1) ? 0 : j + 1; + + // edge direction (XZ) + Vector3 v1; + v1.x = subSpan[jNext].x - subSpan[j].x; + v1.y = 0f; + v1.z = subSpan[jNext].z - subSpan[j].z; + + NormalizeXZInPlace(ref v1); + Vector3 n1 = PerpXZ(v1); + + int vertBase = (j << 2); // j * 4 + + // Fetch top y from roofed vertex (already raised) + // All roof vertices were raised to the same height in GenerateRoofMesh + // Use the passed roofHeight directly to avoid indexing issues with multiple submeshes + float yTop = roofHeight; + float yMin = yTop - wallHeight; + + // Write quads (curr -> next, top and bottom) + // next(1)---------curr(0) + // | | + // nextBot(3)----currBot(2) + + // curr (top) + ref Vector3 v0 = ref vertSpan[vertBase]; + v0.x = subSpan[j].x; + v0.y = yTop; + v0.z = subSpan[j].z; + + // next (top) + ref Vector3 v1t = ref vertSpan[vertBase + 1]; + v1t.x = subSpan[jNext].x; + v1t.y = yTop; + v1t.z = subSpan[jNext].z; + + // curr (bottom) + ref Vector3 v2b = ref vertSpan[vertBase + 2]; + v2b.x = subSpan[j].x; + v2b.y = yMin; + v2b.z = subSpan[j].z; + + // next (bottom) + ref Vector3 v3b = ref vertSpan[vertBase + 3]; + v3b.x = subSpan[jNext].x; + v3b.y = yMin; + v3b.z = subSpan[jNext].z; + + // normals (same for all four) + normSpan[vertBase] = n1; + normSpan[vertBase + 1] = n1; + normSpan[vertBase + 2] = n1; + normSpan[vertBase + 3] = n1; + + // triangles + int si = startIndex; + int baseB = si + ringBase + vertBase; // this edge's first wall vertex + int baseA = si + ringBase + ((j == 0) ? ((count - 1) << 2) : ((j - 1) << 2)); // previous edge's base (for wrapping last pair) + + if (j < count - 1) + { + // two tris for face between curr edge top/bot and next top/bot + trilist[triIndex++] = baseB; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = baseB + 1; + + trilist[triIndex++] = baseB + 1; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = baseB + 3; + } + else + { + // close the ring to the first edge (wrap) + trilist[triIndex++] = baseB; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = si + ringBase; // first edge's top-left + + trilist[triIndex++] = si + ringBase; + trilist[triIndex++] = baseB + 2; + trilist[triIndex++] = si + ringBase + 2; // first edge's bottom-left + } + } + } + + return triIndex; + } + + public int CalculateTriCountFor(int totalPointCount) + { + // 2 triangles per edge quad => 6 indices per edge + // If totalPointCount is number of wall edges, return totalPointCount * 6 + return totalPointCount * 6; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void NormalizeXZInPlace(ref Vector3 v) + { + float lenSq = v.x * v.x + v.z * v.z; + if (lenSq <= 1e-20f) { v.x = 0f; v.z = 0f; return; } + float invLen = 1.0f / Mathf.Sqrt(lenSq); + v.x *= invLen; + v.z *= invLen; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 PerpXZ(in Vector3 v) => new Vector3(-v.z, 0f, v.x); + } +} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta new file mode 100644 index 000000000..d8b87e77e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayHeight.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0acbec66c9f64d47b52e43e9f35874e6 +timeCreated: 1761675661 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs new file mode 100644 index 000000000..73667bc55 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorModule.MeshGeneration; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public class ArrayPolygon + { + public int Polygonize( + Span vertices, + Span normals, + int vertexAnchorIndex, + int[] triList, + int triIndex, + FeatureVertexData vertexData, + float pushUp = 0) + { + int arrayMaxLength = triList.Length; + var submeshes = vertexData.Submeshes; + var allVerts = vertexData.Vertices; + int submeshCount = submeshes.Count; + + if (submeshCount < 2) + return triIndex; + + // Save initial state for rollback on overflow + int initialTriIndex = triIndex; + + var holes = new List(8); + int vertWriteIndex = 0; + bool nextIsHole = false; + + int subStart = submeshes[0]; + int subEnd = submeshes[0]; + + for (int i = 1; i < submeshCount; i++) + { + int prevStart = submeshes[i - 1]; + int prevEnd = submeshes[i]; + int prevLength = prevEnd - prevStart; + + var subVert = allVerts.AsSpan(prevStart, prevLength); + + // Detect the start of a new polygon (outer boundary) + if (IsClockwise(subVert) && vertWriteIndex > 0) + { + var span = allVerts.AsSpan(subStart, subEnd - subStart); + var flatData = EarcutLibrary.Flatten(span); + var result = EarcutLibrary.Earcut(flatData.Vertices, holes, 2); + + if (result.Count + triIndex >= arrayMaxLength) + { + // Roll back partial triangles written by this feature + for (int j = initialTriIndex; j < triIndex; j++) + triList[j] = 0; + return -1; + } + + for (int j = 0; j < result.Count; j++) + triList[triIndex++] = vertexAnchorIndex + result[j]; + + vertexAnchorIndex += flatData.Vertices.Length / 2; + holes.Clear(); + nextIsHole = false; + subStart = prevStart; // new polygon begins here + } + + subEnd = prevEnd; + + if (nextIsHole) + holes.Add(prevStart - subStart); + + nextIsHole = true; + + for (int j = 0; j < prevLength; j++) + { + subVert[j].y += pushUp; + vertices[vertWriteIndex] = subVert[j]; + normals[vertWriteIndex++] = Constants.Math.Vector3Up; + } + } + + // Final polygon flush + var finalSpan = allVerts.AsSpan(subStart, subEnd - subStart); + var finalFlat = EarcutLibrary.Flatten(finalSpan); + var finalResult = EarcutLibrary.Earcut(finalFlat.Vertices, holes, 2); + + if (finalResult.Count + triIndex >= arrayMaxLength) + { + // Roll back partial triangles written by this feature + for (int j = initialTriIndex; j < triIndex; j++) + triList[j] = 0; + return -1; + } + + for (int i = 0; i < finalResult.Count; i++) + triList[triIndex++] = vertexAnchorIndex + finalResult[i]; + + return triIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClockwise(ReadOnlySpan verts) + { + double sum = 0.0; + int count = verts.Length; + + for (int i = 0; i < count; i++) + { + var v1 = verts[i]; + var v2 = verts[(i + 1) % count]; + sum += (v2.x - v1.x) * (v2.z + v1.z); + } + + return sum > 0.0; + } + + + // public int Polygonize(Span vertices, Span normals, int vertexAnchorIndex, int[] triList, int triIndex, PerfVectorFeatureUnity feature) + // { + // var arrayMaxLength = triList.Length; + // var counter = feature.VertexData.Submeshes.Count; + // var holes = new List(); + // int vertStartIndex = 0; + // Data flatData; + // List result; + // var subStart = 0; + // var subEnd = 0; + // var nextIsHole = false; + // + // for (int i = 1; i < counter; i++) + // { + // var subVert = feature.VertexData.Vertices.AsSpan(feature.VertexData.Submeshes[i - 1], + // feature.VertexData.Submeshes[i] - feature.VertexData.Submeshes[i - 1]); + // + // if (IsClockwise(subVert) && vertStartIndex > 0) + // { + // flatData = EarcutLibrary.Flatten(feature.VertexData.Vertices.AsSpan(subStart, subEnd - subStart)); + // result = EarcutLibrary.Earcut(flatData.Vertices, holes, 2); + // + // if (result.Count + triIndex >= arrayMaxLength) return -1; + // + // for (int j = 0; j < result.Count; j++) + // { + // triList[triIndex++] = (vertexAnchorIndex + result[j]); + // } + // + // vertexAnchorIndex += flatData.Vertices.Length / 2; + // holes.Clear(); + // nextIsHole = false; + // subStart = feature.VertexData.Submeshes[i - 1]; + // } + // + // subEnd = feature.VertexData.Submeshes[i]; + // if(nextIsHole) + // { + // holes.Add(feature.VertexData.Submeshes[i - 1] - subStart); + // } + // nextIsHole = true; + // + // for (int j = 0; j < subVert.Length; j++) + // { + // vertices[vertStartIndex + j] = subVert[j]; + // normals[vertStartIndex + j] = Constants.Math.Vector3Up; + // } + // + // vertStartIndex += subVert.Length; + // } + // + // flatData = EarcutLibrary.Flatten(feature.VertexData.Vertices.AsSpan(subStart, subEnd - subStart)); + // result = EarcutLibrary.Earcut(flatData.Vertices, holes, 2); + // + // if (result.Count + triIndex >= arrayMaxLength) return -1; + // for (int i = 0; i < result.Count; i++) + // { + // triList[triIndex++] = (vertexAnchorIndex + result[i]); + // } + // + // return triIndex; + // } + // + // private bool IsClockwise(Span subVert) + // { + // double sum = 0.0; + // var counter = subVert.Length; + // for (int i = 0; i < counter; i++) + // { + // var v1 = subVert[i]; + // var v2 = subVert[(i + 1) % counter]; + // sum += (v2.x - v1.x) * (v2.z + v1.z); + // } + // + // return sum > 0.0; + // } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta new file mode 100644 index 000000000..1b94e22d3 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArrayPolygon.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7acd0c333a6547f3b3d1651b3050db56 +timeCreated: 1744800466 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs new file mode 100644 index 000000000..aaa443181 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs @@ -0,0 +1,35 @@ +using System; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.Modifiers +{ + public class ArraySnapTerrain + { + private IMapInformation _mapInformation; + public ArraySnapTerrain(IMapInformation mapInformation) + { + _mapInformation = mapInformation; + } + + public void SnapTerrain(Span vertices, int topPolyLength, CanonicalTileId tileId, float tileSize) + { + for (var i = 0; i < topPolyLength; i++) + { + ref Vector3 vertex = ref vertices[i]; + var h = 0f; + if (_mapInformation.QueryElevation != null) + { + h = (_mapInformation.QueryElevation( + tileId, + (vertex.x), + (vertex.z + 1)) / _mapInformation.Scale + / tileSize); + } + vertex.Set(vertex.x, h, vertex.z); + } + } + + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta new file mode 100644 index 000000000..40d9b2563 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/Modifiers/ArraySnapTerrain.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6bd110daaa5943c7bb68f3de1d18dc5f +timeCreated: 1756395249 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta new file mode 100644 index 000000000..e47a0aede --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4a0714cd2513450abee8137bcacc20c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs new file mode 100644 index 000000000..0e2452e04 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Mapbox.VectorModule.Filters; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [Serializable] + public class ComponentFilterStack + { + [SerializeReference] + public List Filters = new List(); + public LayerFilterCombinerOperationType Type; + + public ComponentFilterStack() + { + + } + + public void Initialize() + { + foreach (var filter in Filters) + { + filter.Initialize(); + } + } + + public bool Try(RoadFeatureUnity feature) + { + if (Filters == null || Filters.Count == 0) + return true; + + switch (Type) + { + case LayerFilterCombinerOperationType.Any: + for (int i = 0; i < Filters.Count; i++) + { + if (Filters[i].Try(feature)) return true; + } + return false; + case LayerFilterCombinerOperationType.All: + for (int i = 0; i < Filters.Count; i++) + { + if (!Filters[i].Try(feature)) return false; + } + return true; + case LayerFilterCombinerOperationType.None: + for (int i = 0; i < Filters.Count; i++) + { + if (Filters[i].Try(feature)) return false; + } + return true; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta new file mode 100644 index 000000000..b20f205a3 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/ComponentFilterStack.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f90af9963c5434bb7b548779bf95a53 +timeCreated: 1765457934 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs new file mode 100644 index 000000000..582923e09 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs @@ -0,0 +1,555 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorTile.Contants; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using UnityEngine.Rendering; +using Random = System.Random; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + public class RoadComponentVisualizer : MapboxComponentVisualizer + { + private RoadComponentSettings _settings; + + [ThreadStatic] private static Random _threadRandom; + private static Random GetRandom() => _threadRandom ??= new Random(); + + public RoadComponentVisualizer(string name, IMapInformation mapInformation, UnityContext unityContext = null, + RoadComponentSettings settings = null) : base(name, mapInformation, unityContext) + { + _settings = settings ?? new RoadComponentSettings(); + // localPosition (not position) so the y-offset stays in MapRoot-local space + // and composes with any parent transform applied to the map hierarchy. + _layerRootObject.transform.localPosition = new Vector3( + _layerRootObject.transform.localPosition.x, + _settings.GameObjectOffset, + _layerRootObject.transform.localPosition.z); + } + + private enum Turn + { + Start, + Right, + Left + } + + private class MaterialTrianglePair + { + public int[] Triangles; + public int TotalTriSize; + public int[] TriSizes; + public int[] TriRanges; + } + + public override MeshData CreateMesh(CanonicalTileId tileId, VectorTileLayer layer) + { + if (_settings.RoadStyleSheet == null || _settings.RoadStyleSheet.Styles.Count == 0) + return null; + + int classTagIndex = -1; + int typeTagIndex = -1; + int structureTagIndex = -1; + for (var i = 0; i < layer.Keys.Count; i++) + { + var key = layer.Keys[i]; + if (key == "class") classTagIndex = i; + if (key == "type") typeTagIndex = i; + if (key == "structure") structureTagIndex = i; + } + + var rnd = GetRandom(); + var featureCount = layer.FeatureCount(); + var info = new StackMeshInfo(featureCount); + + var triangleGroups = new Dictionary(); + foreach (var style in _settings.RoadStyleSheet.Styles) + { + if (!triangleGroups.ContainsKey(style.Material)) + { + triangleGroups.Add(style.Material, new MaterialTrianglePair() + { + TriSizes = new int[featureCount], + TriRanges = new int[featureCount] + }); + } + } + + var tileSize = Conversions.TileSizeInUnitySpace(tileId.Z, _mapInformation.Scale); + var featureArray = new RoadFeatureUnity[featureCount]; + var styleArray = new RoadStyle[featureCount]; + + for (var i = 0; i < featureCount; i++) + { + var feature = GetFeature(layer, i, ref classTagIndex, ref typeTagIndex, ref structureTagIndex); + if (feature == null) continue; + if (feature.GeometryType != GeomType.LINESTRING) continue; + if (!_settings.RoadStyleSheet.TryGetStyle(feature, out var style)) continue; + + feature.VertexData = feature.Geometry(new Vector3(layer.Extent, 0, -layer.Extent)); + if (feature.VertexData.VertexCount <= 1) continue; + + featureArray[i] = feature; + styleArray[i] = style; + + triangleGroups[style.Material].TriRanges[i] = triangleGroups[style.Material].TotalTriSize; + info.vertexRanges[i] = info.TotalPointCount; + + var vertNeeded = VertexNeed(feature.VertexData); + info.TotalPointCount += vertNeeded; + info.vertexSize[i] = vertNeeded; + if (feature.VertexData.VertexCount == 2) + { + triangleGroups[style.Material].TriSizes[i] = 12 + 6; + triangleGroups[style.Material].TotalTriSize += 12 + 6; + info.TotalTriangleCount += 12 + 6; //2 tri 3 vert + 12 for caps + } + else + { + //12 tri for start/end caps + //6 tri end line segment + // 12 per middle vertex + var triSize = 12 + 6 + ((feature.VertexData.VertexCount - 2) * 12); + triangleGroups[style.Material].TriSizes[i] = triSize; + triangleGroups[style.Material].TotalTriSize += triSize; + info.TotalTriangleCount += triSize; + } + } + + foreach (var pair in triangleGroups) + { + pair.Value.Triangles = new int[pair.Value.TotalTriSize]; + } + + var meshData = new MeshData(info, info.TotalPointCount) + { + UVs = new Vector2[info.TotalPointCount] + }; + + var featureTriIndex = 0; + for (int i = 0; i < featureArray.Length; i++) + { + var featureResult = featureArray[i]; + var style = styleArray[i]; + + if (featureResult == null) continue; + if (featureResult.VertexData.VertexCount <= 1) continue; + + var group = triangleGroups[style.Material]; + + var scaledRoadWidth = style.Width / _mapInformation.Scale / tileSize; + var designedVertCount = meshData.MeshInfo.vertexSize[i]; + if (designedVertCount <= 2 || group.TriSizes[i] < 3) continue; + + var vertices = meshData.Vertices.AsSpan(meshData.MeshInfo.vertexRanges[i], designedVertCount); + var normals = meshData.Normals.AsSpan(meshData.MeshInfo.vertexRanges[i], designedVertCount); + var uvs = meshData.UVs.AsSpan(meshData.MeshInfo.vertexRanges[i], designedVertCount); + var tris = group.Triangles.AsSpan(group.TriRanges[i], group.TriSizes[i]); + + var lastTurnWas = Turn.Start; + var prevFirst = -2; + var prevSecond = -1; + var subVertIndex = 0; + var subTriIndex = 0; + + for (var j = 0; j < featureResult.VertexData.Submeshes.Count - 1; j++) + { + var subSize = featureResult.VertexData.Submeshes[j + 1] - featureResult.VertexData.Submeshes[j]; + var startVertex = featureResult.VertexData.Submeshes[j]; + + var elevation = style.PushUp + (float)rnd.NextDouble() * _settings.RandomOffsetRange; + var sideNormal = new Vector3(0, 0, 0); + var finishLine = false; + var distance = 0f; + Vector3 dirNext = new Vector3(0, 0, 0); + Vector3 dirPrev; + for (int k = 0; k < subSize - 1; k++) + { + var current = featureResult.VertexData.Vertices[startVertex + k]; + var next = featureResult.VertexData.Vertices[startVertex + k + 1]; + + + + if(k > 0) + { + //var prev = featureResult.VertexData.Vertices[startVertex + k - 1]; + var movement = next - current; + dirPrev = -dirNext; //(prev - current).normalized; + dirNext = movement.normalized; + var dirInside = (dirNext + dirPrev).normalized * scaledRoadWidth; + var prevSideNormal = sideNormal; + sideNormal = new Vector3(dirNext.z * scaledRoadWidth, 0, -dirNext.x * scaledRoadWidth); + + // decide previous triangle indices based on turnState + if (lastTurnWas == Turn.Right) + { + prevFirst = -1; + prevSecond = -2; + } + else if (lastTurnWas == Turn.Left || lastTurnWas == Turn.Start) + { + prevFirst = -2; + prevSecond = -1; + } + + int baseVertIndex = subVertIndex; + int baseTriIndex = featureTriIndex + baseVertIndex; + + //featureTriIndex is the start of the feature + //baseVertIndex is the start of this section (like corner) + + // precompute side offsets with sign depending on isRight + var isRight = Vector3.Dot(prevSideNormal, dirNext) > 0; + int sign = isRight ? -1 : 1; + + // v0: current ± prevSideNormal + float v0x = current.x + sign * prevSideNormal.x; + float v0z = current.z + sign * prevSideNormal.z; + + // v2: current ± sideNormal + float v2x = current.x + sign * sideNormal.x; + float v2z = current.z + sign * sideNormal.z; + + // v1: current - dirInside + float v1x = current.x - dirInside.x; + float v1z = current.z - dirInside.z; + + // v3: current + dirInside + float v3x = current.x + dirInside.x; + float v3z = current.z + dirInside.z; + + // write vertices + vertices[baseVertIndex ] = new Vector3(v0x, elevation, v0z); + vertices[baseVertIndex + 1] = new Vector3(v1x, elevation, v1z); + vertices[baseVertIndex + 2] = new Vector3(v2x, elevation, v2z); + vertices[baseVertIndex + 3] = new Vector3(v3x, elevation, v3z); + + // write normals (all up) + normals[baseVertIndex ] =Vector3.up; + normals[baseVertIndex + 1] =Vector3.up; + normals[baseVertIndex + 2] =Vector3.up; + normals[baseVertIndex + 3] = Vector3.up; + + // write uvs + uvs[baseVertIndex ] = new Vector2(sign == 1 ? 0 : 1, distance); + uvs[baseVertIndex + 1] = new Vector2(sign == 1 ? 0 : 1, distance); + uvs[baseVertIndex + 2] = new Vector2(sign == 1 ? 0 : 1, distance); + uvs[baseVertIndex + 3] = new Vector2(sign == 1 ? 1 : 0, distance); + + // handy local vars for indices + int i0 = baseTriIndex + 0; + int i1 = baseTriIndex + 1; + int i2 = baseTriIndex + 2; + int i3 = baseTriIndex + 3; + int ipf = baseTriIndex + prevFirst; + int ips = baseTriIndex + prevSecond; + + // prev tris + corner tris + if (isRight) + { + // prev tris + tris[subTriIndex++] = ipf; + tris[subTriIndex++] = ips; + tris[subTriIndex++] = i0; + + tris[subTriIndex++] = i0; + tris[subTriIndex++] = i3; + tris[subTriIndex++] = ipf; + + // corner tris + tris[subTriIndex++] = i0; + tris[subTriIndex++] = i1; + tris[subTriIndex++] = i3; + + tris[subTriIndex++] = i1; + tris[subTriIndex++] = i2; + tris[subTriIndex++] = i3; + + lastTurnWas = Turn.Right; + } + else + { + // prev tris + tris[subTriIndex++] = i0; + tris[subTriIndex++] = ipf; + tris[subTriIndex++] = ips; + + tris[subTriIndex++] = i3; + tris[subTriIndex++] = i0; + tris[subTriIndex++] = ips; + + // corner tris + tris[subTriIndex++] = i0; + tris[subTriIndex++] = i3; + tris[subTriIndex++] = i1; + + tris[subTriIndex++] = i2; + tris[subTriIndex++] = i1; + tris[subTriIndex++] = i3; + + lastTurnWas = Turn.Left; + } + + + subVertIndex += 4; + + if (k == subSize - 2) + { + finishLine = true; + } + + distance += movement.magnitude; + } + else if (k == 0) + { + distance = 0; + var movement = next - current; + dirNext = movement.normalized; + sideNormal = new Vector3(dirNext.z * scaledRoadWidth, 0, -dirNext.x * scaledRoadWidth); + + // round caps are width/2 back and then width/2 to sides + var capSide1x = current.x - (dirNext.x * scaledRoadWidth/2) + (sideNormal.x/2); + var capSide1z = current.z - (dirNext.z * scaledRoadWidth/2) + (sideNormal.z/2); + var capSide2x = current.x - (dirNext.x * scaledRoadWidth/2) - (sideNormal.x/2); + var capSide2z = current.z - (dirNext.z * scaledRoadWidth/2) - (sideNormal.z/2); + var side1x = current.x + sideNormal.x; + var side1z = current.z + sideNormal.z; + var side2x = current.x - sideNormal.x; + var side2z = current.z - sideNormal.z; + + vertices[subVertIndex ] = new Vector3(capSide1x, elevation, capSide1z); + vertices[subVertIndex + 1] = new Vector3(capSide2x, elevation, capSide2z); + vertices[subVertIndex + 2] = new Vector3(side1x, elevation, side1z); + vertices[subVertIndex + 3] = new Vector3(side2x, elevation, side2z); + + normals[subVertIndex ] = Vector3.up; + normals[subVertIndex + 1] = Vector3.up; + normals[subVertIndex + 2] = Vector3.up; + normals[subVertIndex + 3] = Vector3.up; + + //0.25 and 0.75 are just pushing uv coordinates inside a litte to prevent ugly stretching on caps + uvs[subVertIndex ] = new Vector2(0.25f, 0); + uvs[subVertIndex + 1] = new Vector2(0.75f, 0); + uvs[subVertIndex + 2] = new Vector2(0, 0); + uvs[subVertIndex + 3] = new Vector2(1, 0); + + var baseTriIndex = featureTriIndex + subVertIndex; + tris[subTriIndex++] = baseTriIndex + 0; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 2; + + tris[subTriIndex++] = baseTriIndex + 2; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 3; + + subVertIndex += 4; + lastTurnWas = 0; + + if (subSize == 2) + finishLine = true; + + distance += movement.magnitude; + } + + if (finishLine) + { + current = featureResult.VertexData.Vertices[startVertex + k + 1]; + var dir = (current - featureResult.VertexData.Vertices[startVertex + k]).normalized; + + + if (lastTurnWas == Turn.Left || lastTurnWas == 0) + { + prevFirst = -1; + prevSecond = -2; + } + else if (lastTurnWas == Turn.Right) + { + prevFirst = -2; + prevSecond = -1; + } + + var side1x = current.x + sideNormal.x; + var side1z = current.z + sideNormal.z; + var side2x = current.x - sideNormal.x; + var side2z = current.z - sideNormal.z; + + var capSide1x = current.x + (dir.x * scaledRoadWidth / 2) + (sideNormal.x / 2); + var capSide1z = current.z + (dir.z * scaledRoadWidth / 2) + (sideNormal.z / 2); + var capSide2x = current.x + (dir.x * scaledRoadWidth / 2) - (sideNormal.x / 2); + var capSide2z = current.z + (dir.z * scaledRoadWidth / 2) - (sideNormal.z / 2); + + vertices[subVertIndex ] = new Vector3(side1x, elevation, side1z); + vertices[subVertIndex + 1] = new Vector3(side2x, elevation, side2z); + vertices[subVertIndex + 2] = new Vector3(capSide1x, elevation, capSide1z); + vertices[subVertIndex + 3] = new Vector3(capSide2x, elevation, capSide2z); + + normals[subVertIndex ] = Vector3.up; + normals[subVertIndex + 1] = Vector3.up; + normals[subVertIndex + 2] = Vector3.up; + normals[subVertIndex + 3] = Vector3.up; + + uvs[subVertIndex ] = new Vector2(0, distance); + uvs[subVertIndex + 1] = new Vector2(1, distance); + //0.25 and 0.75 are just pushing uv coordinates inside a litte to prevent ugly stretching on caps + uvs[subVertIndex + 2] = new Vector2(0.25f, distance + scaledRoadWidth/2); + uvs[subVertIndex + 3] = new Vector2(0.75f, distance + scaledRoadWidth/2); + + var baseTriIndex = featureTriIndex + subVertIndex; + tris[subTriIndex++] = baseTriIndex + prevSecond; + tris[subTriIndex++] = baseTriIndex + prevFirst; + tris[subTriIndex++] = baseTriIndex + 0; + + tris[subTriIndex++] = baseTriIndex + 0; + tris[subTriIndex++] = baseTriIndex + prevFirst; + tris[subTriIndex++] = baseTriIndex + 1; + + tris[subTriIndex++] = baseTriIndex + 0; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 2; + + tris[subTriIndex++] = baseTriIndex + 2; + tris[subTriIndex++] = baseTriIndex + 1; + tris[subTriIndex++] = baseTriIndex + 3; + + subVertIndex += 4; + } + } + } + + featureTriIndex += designedVertCount; + } + + foreach (var pair in triangleGroups) + { + meshData.Materials.Add(pair.Key); + meshData.Triangles.Add(pair.Value.Triangles); + } + + //TODO we are keeping triangle information inside groups + //and not pass it into stackinfo + //this'll probably break mouse click interactions + //planned with meshinfo.tri information in mind + //currently when user clicks and we get triIndex info from hit + //there'll be no data to match it, and highlight selection or something + + return meshData; + } + + private int VertexNeed(FeatureVertexData data) + { + var total = 0; + for (var i = 0; i < data.Submeshes.Count - 1; i++) + { + var size = data.Submeshes[i + 1] - data.Submeshes[i]; + total += 4 * size; //(size - 2) * 4 + (2 * 2); //two vertices for cap, 4 for each mid vertex + } + + return total; + } + + public override List CreateGo(CanonicalTileId tileId, MeshData meshData) + { + var objectList = new List(); + var entity = _buildingObjectPool.GetObject(); + var mats = new Material[meshData.Triangles.Count]; + // for (int i = 0; i < meshData.Triangles.Count; i++) + // { + // mats[i] = _settings.Material; + // } + for (var i = 0; i < meshData.Triangles.Count; i++) + { + mats[i] = meshData.Materials[i]; + } + + entity.MeshRenderer.materials = mats; + + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); + entity.StackId = 0; + + var mesh = entity.Mesh; + mesh.Clear(); + mesh.SetVertices(meshData.Vertices); + mesh.SetNormals(meshData.Normals); + mesh.SetUVs(0, meshData.UVs); + mesh.indexFormat = IndexFormat.UInt32; + + mesh.subMeshCount = meshData.Triangles.Count; + for (var index = 0; index < meshData.Triangles.Count; index++) + { + var submesh = meshData.Triangles[index]; + mesh.SetTriangles(submesh, index); + } + + entity.MeshFilter.sharedMesh = mesh; + objectList.Add(entity.GameObject); + + if (!_results.ContainsKey(tileId)) + _results.Add(tileId, new List()); + _results[tileId].Add(entity); + OnBuildingMeshCreated(tileId, entity.GameObject, meshData); + + // foreach (var vertex in meshData.Vertices) + // { + // var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); + // go.transform.position = vertex; + // go.transform.localScale = Vector3.one * 0.001f; + // go.transform.SetParent(entity.Transform); + // } + + return objectList; + } + + private RoadFeatureUnity GetFeature(VectorTileLayer layer, int i, ref int classTagIndex, ref int typeTagIndex, ref int structureTagIndex) + { + var view = layer.GetViewFor(i); + var layerData = layer.Data.Slice(view.x, view.y); + var featureReader = new PbfReader(layerData); + var feature = new RoadFeatureUnity(); + bool geomTypeSet = false; + while (featureReader.NextByte()) + { + int featureType = featureReader.Tag; + switch ((FeatureType)featureType) + { + case FeatureType.Id: + feature.Id = (ulong)featureReader.Varint(); + break; + case FeatureType.Tags: + var tags = featureReader.GetPackedInt(); + feature.Tags = tags; + break; + case FeatureType.Type: + int geomType = (int)featureReader.Varint(); + feature.GeometryType = (GeomType)geomType; + geomTypeSet = true; + break; + case FeatureType.Geometry: + if (null != feature.GeometryCommands) + { + throw new Exception(string.Format("Layer [{0}], feature already has a geometry", + layer.Name)); + } + + //get raw array of commands and coordinates + feature.GeometryCommands = featureReader.GetPackedUnit32(); + break; + default: + featureReader.Skip(); + break; + } + } + + // Discard features with missing/malformed geometry + if (feature.GeometryCommands == null) return null; + + feature.SetProperties(ref layer, ref classTagIndex, ref typeTagIndex, ref structureTagIndex); + return feature; + } + } +} diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta new file mode 100644 index 000000000..cc6700e94 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fb3c26c6e32644694aa0988af913c9a6 +timeCreated: 1744797730 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs new file mode 100644 index 000000000..4d2306a5e --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Unity; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.Filters; +using Mapbox.VectorModule.Unity; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [DisplayName("Road Component")] + [CreateAssetMenu(menuName = "Mapbox/Layer Visualizers/Road Component Visualizer")] + public class RoadComponentVisualizerObject : LayerVisualizerConstructor, IPerformanceLayerVisualizer + { + public RoadComponentSettings Settings; + public bool IsActive = true; + + private RoadComponentVisualizer _layerVisualizer; + + public IVectorLayerVisualizer GetLayerVisualizer() + { + return _layerVisualizer; + } + + public override IVectorLayerVisualizer ConstructLayerVisualizer(IMapInformation mapInformation, UnityContext unityContext) + { + Settings.RoadStyleSheet.Initialize(); + _layerVisualizer = new RoadComponentVisualizer("road", mapInformation, unityContext, Settings); + _layerVisualizer.Active = IsActive; + + _layerVisualizer.OnVectorMeshCreated += OnVectorMeshCreated; + _layerVisualizer.OnVectorMeshDestroyed += OnVectorMeshDestroyed; + + return _layerVisualizer; + } + + public Action OnVectorMeshCreated = list => { }; + public Action OnVectorMeshDestroyed = go => { }; + } + + [Serializable] + public class RoadComponentSettings + { + [Tooltip("Vertical offset to be applied to the road component.")] + public float GameObjectOffset; + [Tooltip("Road segments will be offset by a small amount to prevent z-fighting issues.")] + public float RandomOffsetRange = 0.0001f; + public RoadStyleSheet RoadStyleSheet; + + public RoadComponentSettings() + { + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta new file mode 100644 index 000000000..87a0a0fe8 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadComponentVisualizerObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e926a31006c7a440ea45ffe7b2bcfff6 +timeCreated: 1744829680 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs new file mode 100644 index 000000000..92db1719f --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs @@ -0,0 +1,49 @@ +using Mapbox.VectorModule.ComponentSystem.Data; +using Mapbox.VectorTile.Geometry; +using UnityEngine; +using DecodeGeometry = Mapbox.VectorModule.ComponentSystem.Data.DecodeGeometry; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + public class RoadFeatureUnity + { + public ulong Id; + public GeomType GeometryType; + public uint[] GeometryCommands; + public FeatureVertexData VertexData; + public int[] Tags; + public string Class; + public string Type; + public string Structure; + + public void SetProperties(ref VectorTileLayer layer, ref int classTagIndex, ref int typeTagIndex, ref int structureTagIndex) + { + if (Tags == null) return; // Some features have no tags + + var tagCount = Tags.Length; + //some features have odd number of tags + //not sure if it's a bug or data issue + //so -1 here to skip last single tag + for (int i = 0; i < tagCount - 1; i += 2) + { + if (classTagIndex != -1 && Tags[i] == classTagIndex) + { + Class = layer.Values[Tags[i + 1]].ToString(); + } + else if (typeTagIndex != -1 && Tags[i] == typeTagIndex) + { + Type = layer.Values[Tags[i + 1]].ToString(); + } + else if (structureTagIndex != -1 && Tags[i] == structureTagIndex) + { + Structure = layer.Values[Tags[i + 1]].ToString(); + } + } + } + + public FeatureVertexData Geometry(Vector3 scale) + { + return DecodeGeometry.GetGeometry(GeometryCommands, scale); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta new file mode 100644 index 000000000..5b24a6479 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFeatureUnity.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5fb907227ade4bbfa628c03e59c903fa +timeCreated: 1765457966 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs new file mode 100644 index 000000000..9bc037c1a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Mapbox.VectorModule.Filters; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [Serializable] + public abstract class RoadFilter + { + public RoadFilter() + { + + } + + public abstract void Initialize(); + public abstract bool Try(RoadFeatureUnity feature); + } + + [Serializable] + [DisplayName("Class Filter")] + public class RoadClassFilter : RoadFilter + { + public StringCheckOperation CheckOperation = StringCheckOperation.Equals; + public string FilterString; + public bool Invert; + private HashSet _types = new HashSet(); + + public override void Initialize() + { + FilterString = FilterString.ToLowerInvariant(); + if (CheckOperation == StringCheckOperation.Contains) + { + _types = new HashSet(); + foreach (var s in FilterString.Split(',')) + { + _types.Add(s.Trim().ToLowerInvariant()); + } + } + } + + public override bool Try(RoadFeatureUnity feature) + { + if (feature.Class == null) return false; + + var result = false; + if (CheckOperation == StringCheckOperation.Equals) + { + result = FilterString == feature.Class; + } + else if (CheckOperation == StringCheckOperation.Contains) + { + result = _types.Contains(feature.Class); + } + + return !Invert ? result : !result; + } + } + + [Serializable] + [DisplayName("Structure Filter")] + public class RoadStructureFilter : RoadFilter + { + public StringCheckOperation CheckOperation = StringCheckOperation.Equals; + public string FilterString; + public bool Invert; + private HashSet _types = new HashSet(); + + public override void Initialize() + { + FilterString = FilterString.ToLowerInvariant(); + if (CheckOperation == StringCheckOperation.Contains) + { + _types = new HashSet(); + foreach (var s in FilterString.Split(',')) + { + _types.Add(s.Trim().ToLowerInvariant()); + } + } + } + + public override bool Try(RoadFeatureUnity feature) + { + if (feature.Structure == null) return false; + + var result = false; + if (CheckOperation == StringCheckOperation.Equals) + { + result = FilterString == feature.Structure; + } + else if (CheckOperation == StringCheckOperation.Contains) + { + result = _types.Contains(feature.Structure); + } + + return !Invert ? result : !result; + } + } + + [Serializable] + [DisplayName("Type Filter")] + public class RoadTypeFilter : RoadFilter + { + public StringCheckOperation CheckOperation = StringCheckOperation.Equals; + public string FilterString; + public bool Invert; + private HashSet _types = new HashSet(); + + public override void Initialize() + { + FilterString = FilterString.ToLowerInvariant(); + if (CheckOperation == StringCheckOperation.Contains) + { + _types = new HashSet(); + foreach (var s in FilterString.Split(',')) + { + _types.Add(s.Trim().ToLowerInvariant()); + } + } + } + + public override bool Try(RoadFeatureUnity feature) + { + if (feature.Type == null) return false; + + var result = false; + if (CheckOperation == StringCheckOperation.Equals) + { + result = FilterString == feature.Type; + } + else if (CheckOperation == StringCheckOperation.Contains) + { + result = _types.Contains(feature.Type); + } + + return !Invert ? result : !result; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta new file mode 100644 index 000000000..713e6eab2 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadFilter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bc1f26eac70c4ed88fcec9939dfbd8a6 +timeCreated: 1765457941 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs new file mode 100644 index 000000000..38d5b6f6c --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs @@ -0,0 +1,22 @@ +using System; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [Serializable] + public class RoadStyle + { + [NonSerialized] public int RuntimeId; + [Tooltip("The name of the road style (used only for the UI)")] + public string Name; + public ComponentFilterStack Filters; + public float Width; + public float PushUp; + public Material Material; + + public void Initialize() + { + Filters.Initialize(); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta new file mode 100644 index 000000000..bb0d5fcfe --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyle.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 14e833ab58144fdda5c1465744f677ad +timeCreated: 1765457926 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs new file mode 100644 index 000000000..531c6f1a4 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using UnityEngine; + +namespace Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer +{ + [DisplayName("Road Style Sheet")] + [CreateAssetMenu(menuName = "Mapbox/Road Style Sheet")] + public class RoadStyleSheet : ScriptableObject + { + public List Styles; + + public void Initialize() + { + foreach (var style in Styles) + { + style.Initialize(); + } + } + + public bool TryGetStyle(RoadFeatureUnity feature, out RoadStyle style) + { + for (int i = 0; i < Styles.Count; i++) + { + if (Styles[i].Filters.Try(feature)) + { + style = Styles[i]; + return true; + } + } + style = null; + return false; + } + + public bool Contains(RoadFeatureUnity feature) + { + for (int i = 0; i < Styles.Count; i++) + { + if (Styles[i].Filters.Try(feature)) + { + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta new file mode 100644 index 000000000..14aeac370 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/ComponentSystem/RoadComponentVisualizer/RoadStyleSheet.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bf8c281932374fb0b5c80fa7fbca2b5b +timeCreated: 1765441634 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs new file mode 100644 index 000000000..7b7dad175 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs @@ -0,0 +1,307 @@ +#if UNITY_EDITOR +using System; +using System.ComponentModel; +using System.Linq; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; +using UnityEditor; +using UnityEngine; +using Mapbox.VectorModule.MeshGeneration.Unity; +using Mapbox.VectorModule.Filters; // where FilterBase lives + +[CustomPropertyDrawer(typeof(ComponentFilterStack))] +public class ComponentFilterStackDrawer : PropertyDrawer +{ + private const float LineSpacing = 2f; + private const float BoxPadding = 4f; + private const float HeaderHeight = 18f; + + // Cached FilterBase-derived types + private static Type[] _filterTypes; + private static string[] _filterTypePaths; + private static bool _typesInitialized; + + private static void EnsureFilterTypes() + { + if (_typesInitialized) + return; + + _typesInitialized = true; + + var baseType = typeof(RoadFilter); + + var types = TypeCache.GetTypesDerivedFrom() + .Where(t => t != null && !t.IsAbstract) + .ToArray(); + + _filterTypes = types; + + _filterTypePaths = _filterTypes + .Select(t => + { + // Use DisplayNameAttribute if present for pretty menu paths + var attr = t + .GetCustomAttributes(typeof(DisplayNameAttribute), true) + .FirstOrDefault() as DisplayNameAttribute; + return attr != null && !string.IsNullOrEmpty(attr.DisplayName) + ? attr.DisplayName + : t.Name; + }) + .ToArray(); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + EnsureFilterTypes(); + + var typeProp = property.FindPropertyRelative("Type"); + var filtersProp = property.FindPropertyRelative("Filters"); + + // Foldout + Rect foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label, true); + + if (!property.isExpanded) + { + EditorGUI.EndProperty(); + return; + } + + EditorGUI.indentLevel++; + + float y = foldoutRect.y + EditorGUIUtility.singleLineHeight + LineSpacing; + float width = position.width; + + // Combiner type + if (typeProp != null) + { + Rect typeRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(typeRect, typeProp, new GUIContent("Process if")); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + if (filtersProp == null) + { + Rect warnRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight * 2f); + EditorGUI.HelpBox(warnRect, + "Filters list not found.\n" + + "Ensure it's declared as:\n[SerializeReference] public List Filters;", + MessageType.Warning); + y += warnRect.height + LineSpacing; + } + else + { + // Filters label + // Rect labelRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + // EditorGUI.LabelField(labelRect, "Filters", EditorStyles.boldLabel); + // y += EditorGUIUtility.singleLineHeight + LineSpacing; + + if (filtersProp.arraySize == 0) + { + Rect infoRect = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight * 1.5f); + EditorGUI.HelpBox(infoRect, "No filters. Use 'Add Filter' to create one.", MessageType.Info); + y += infoRect.height + LineSpacing; + } + else + { + // Draw each filter as a nice box + for (int i = 0; i < filtersProp.arraySize; i++) + { + SerializedProperty element = filtersProp.GetArrayElementAtIndex(i); + float elementHeight = EditorGUI.GetPropertyHeight(element, true); + + float boxHeight = HeaderHeight + elementHeight + BoxPadding * 2f; + Rect boxRect = new Rect(position.x, y, width, boxHeight); + GUI.Box(boxRect, GUIContent.none); + + float innerX = boxRect.x + BoxPadding; + float innerY = boxRect.y + BoxPadding; + float innerW = boxRect.width - BoxPadding * 2f; + + if (element != null) + { + // Header: "Filter i (TypeName)" + Remove button + string typeName = GetManagedReferenceTypeName(element) ?? "Null"; + string header = string.Format("{0} - {1}", typeName, element.FindPropertyRelative("FilterString")?.stringValue); + + Rect headerLabelRect = new Rect(innerX, innerY, innerW - 70f, EditorGUIUtility.singleLineHeight); + EditorGUI.LabelField(headerLabelRect, header, EditorStyles.boldLabel); + } + + Rect removeRect = new Rect(innerX + innerW - 65f, innerY, 60f, + EditorGUIUtility.singleLineHeight); + if (GUI.Button(removeRect, "Remove")) + { + filtersProp.DeleteArrayElementAtIndex(i); + property.serializedObject.ApplyModifiedProperties(); + EditorGUI.indentLevel--; + EditorGUI.EndProperty(); + return; + } + + innerY += HeaderHeight - (EditorGUIUtility.singleLineHeight - LineSpacing); + + // Filter fields + Rect contentRect = new Rect(innerX, innerY, innerW, elementHeight); + EditorGUI.PropertyField(contentRect, element, GUIContent.none, true); + + y += boxHeight + LineSpacing; + } + } + + // Add / Clear buttons + float buttonHeight = EditorGUIUtility.singleLineHeight; + float halfWidth = (width - 2f) * 0.5f; + + Rect addRect = new Rect(position.x, y, halfWidth, buttonHeight); + //Rect clearRect = new Rect(position.x + halfWidth + 2f, y, halfWidth, buttonHeight); + + if (GUI.Button(addRect, "Add Filter")) + { + ShowAddMenu(addRect, filtersProp, property.serializedObject); + } + + //GUI.enabled = filtersProp.arraySize > 0; + // if (GUI.Button(clearRect, "Clear All")) + // { + // filtersProp.ClearArray(); + // property.serializedObject.ApplyModifiedProperties(); + // } + //GUI.enabled = true; + + y += buttonHeight + LineSpacing; + } + + EditorGUI.indentLevel--; + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + float height = EditorGUIUtility.singleLineHeight; // foldout + + if (!property.isExpanded) + return height; + + float spacing = LineSpacing; + var typeProp = property.FindPropertyRelative("Type"); + var filtersProp = property.FindPropertyRelative("Filters"); + + height += spacing; // after foldout + + if (typeProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (filtersProp == null) + { + height += EditorGUIUtility.singleLineHeight * 2f + spacing; + return height; + } + + // "Filters" label + height += EditorGUIUtility.singleLineHeight + spacing; + + if (filtersProp.arraySize == 0) + { + height += EditorGUIUtility.singleLineHeight * 1.5f + spacing; + } + else + { + for (int i = 0; i < filtersProp.arraySize; i++) + { + var element = filtersProp.GetArrayElementAtIndex(i); + float elementHeight = EditorGUI.GetPropertyHeight(element, true); + float boxHeight = HeaderHeight + elementHeight + BoxPadding * 2f; + height += boxHeight + spacing; + } + } + + // Add / Clear buttons + height += EditorGUIUtility.singleLineHeight + spacing; + + return height; + } + + // --------- Add menu + helpers --------- + + private static void ShowAddMenu(Rect buttonRect, SerializedProperty filtersProp, SerializedObject serializedObject) + { + EnsureFilterTypes(); + + if (_filterTypes == null || _filterTypes.Length == 0) + { + EditorUtility.DisplayDialog( + "No filter types found", + "No non-abstract RoadFilter implementations found in loaded assemblies.", + "OK"); + return; + } + + // Need a stable reference to re-find the property later + var target = serializedObject.targetObject; + string propPath = filtersProp.propertyPath; + + GenericMenu menu = new GenericMenu(); + + for (int i = 0; i < _filterTypes.Length; i++) + { + Type type = _filterTypes[i]; + string path = _filterTypePaths[i]; + menu.AddItem(new GUIContent(path), false, () => + { + AddFilterOfType(target, propPath, type); + }); + } + + menu.DropDown(buttonRect); + } + + private static void AddFilterOfType(UnityEngine.Object target, string filtersPropPath, Type type) + { + if (target == null || type == null) + return; + + var so = new SerializedObject(target); + var filtersProp = so.FindProperty(filtersPropPath); + if (filtersProp == null || !filtersProp.isArray) + return; + + int index = filtersProp.arraySize; + filtersProp.InsertArrayElementAtIndex(index); + SerializedProperty element = filtersProp.GetArrayElementAtIndex(index); + + if (element.propertyType == SerializedPropertyType.ManagedReference) + { + object instance = Activator.CreateInstance(type); + element.managedReferenceValue = instance; + } + else + { + // Wrong setup; clean up and bail + filtersProp.DeleteArrayElementAtIndex(index); + } + + so.ApplyModifiedProperties(); + } + + private static string GetManagedReferenceTypeName(SerializedProperty prop) + { + if (!prop.hasMultipleDifferentValues && !string.IsNullOrEmpty(prop.managedReferenceFullTypename)) + { + // "AssemblyName TypeFullName" + string fullName = prop.managedReferenceFullTypename; + int spaceIndex = fullName.IndexOf(' '); + if (spaceIndex >= 0 && spaceIndex < fullName.Length - 1) + { + string typeName = fullName.Substring(spaceIndex + 1); + int lastDot = typeName.LastIndexOf('.'); + if (lastDot >= 0 && lastDot < typeName.Length - 1) + return typeName.Substring(lastDot + 1); + return typeName; + } + } + return null; + } +} +#endif diff --git a/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta new file mode 100644 index 000000000..352381eff --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/ComponentFilterStackDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a086f27c0bb4aff9fb799c63f34baa0 +timeCreated: 1765445147 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs new file mode 100644 index 000000000..4a003e8ee --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs @@ -0,0 +1,95 @@ +using System; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using UnityEditor; +using UnityEngine; + +namespace Mapbox.VectorModule.Editor +{ + [CustomPropertyDrawer(typeof(GeometryExtrusionOptions))] + public class GeometryExtrusionOptionsDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + GUILayout.Space(-EditorGUIUtility.singleLineHeight - 2); + + // Find properties + var extrusionTypeProp = property.FindPropertyRelative("extrusionType"); + var geomTypeProp = property.FindPropertyRelative("extrusionGeometryType"); + var propertyNameProp = property.FindPropertyRelative("propertyName"); + var minHeightProp = property.FindPropertyRelative("minimumHeight"); + var maxHeightProp = property.FindPropertyRelative("maximumHeight"); + var scaleFactorProp = property.FindPropertyRelative("extrusionScaleFactor"); + + EditorGUILayout.PropertyField(geomTypeProp); + + //part to render extrusion type in dropbox without none setting + { + // Get all enum values except 'None' (index 0) + var enumValues = (ExtrusionType[])Enum.GetValues(typeof(ExtrusionType)); + var enumNames = Enum.GetNames(typeof(ExtrusionType)); + + // Skip "None" + var displayedValues = new ExtrusionType[enumValues.Length - 1]; + var displayedNames = new string[enumValues.Length - 1]; + Array.Copy(enumValues, 1, displayedValues, 0, displayedValues.Length); + Array.Copy(enumNames, 1, displayedNames, 0, displayedNames.Length); + + // Determine current value + var currentType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + // If current value is None, default to first visible option + if (currentType == ExtrusionType.None) + currentType = displayedValues[0]; + + // Draw dropdown without "None" + int selectedIndex = Array.IndexOf(displayedValues, currentType); + int newIndex = EditorGUILayout.Popup("Extrusion Type", Mathf.Max(selectedIndex, 0), displayedNames); + + extrusionTypeProp.enumValueIndex = (int)displayedValues[newIndex]; + } + + // Determine which ExtrusionType is selected + var extrusionType = (ExtrusionType)extrusionTypeProp.enumValueIndex; + + DrawReadOnlyProperty(propertyNameProp); + + // Show common field for all types that need propertyName + switch (extrusionType) + { + case ExtrusionType.PropertyHeight: + + break; + + case ExtrusionType.AbsoluteHeight: + EditorGUILayout.PropertyField(maxHeightProp, new GUIContent("Height")); + break; + + case ExtrusionType.MinHeight: + EditorGUILayout.PropertyField(minHeightProp); + break; + + case ExtrusionType.MaxHeight: + EditorGUILayout.PropertyField(maxHeightProp); + break; + case ExtrusionType.RangeHeight: + EditorGUILayout.PropertyField(minHeightProp); + EditorGUILayout.PropertyField(maxHeightProp); + break; + + default: + break; + } + + void DrawReadOnlyProperty(SerializedProperty prop) + { + GUI.enabled = false; + EditorGUILayout.PropertyField(prop); + GUI.enabled = true; + } + + EditorGUI.EndProperty(); + } + } +} diff --git a/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta new file mode 100644 index 000000000..f80329b4a --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/GeometryExtrusionOptionsDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ecf2256f8fa924ec1832c2fcb6b5d36f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs b/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs index bc9c7120c..7ee0c5eaf 100644 --- a/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs +++ b/Runtime/Mapbox/VectorModule/Editor/HeightModifierEditor.cs @@ -18,6 +18,11 @@ public class HeightModifierObjectEditor : UnityEditor.Editor void OnEnable() { + if (targets[0] == null) + { + return; + } + // Cache the main SerializedProperties extrusionOptionsProp = serializedObject.FindProperty("ExtrusionOptions"); extrusionTypeProp = extrusionOptionsProp.FindPropertyRelative("extrusionType"); diff --git a/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef b/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef index 682b489e8..42a069f9f 100644 --- a/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef +++ b/Runtime/Mapbox/VectorModule/Editor/Mapbox.VectorModule.Editor.asmdef @@ -4,7 +4,8 @@ "references": [ "GUID:2400806fb903448e5b7e737b92f1e434", "GUID:dcd3af994807b4804b1e69cd4a2bdfed", - "GUID:093b9fb2cd4794a8c94472d55c8bb0a9" + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:5aef8304858a8459cb2204d15bf75ab9" ], "includePlatforms": [ "Editor" diff --git a/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs new file mode 100644 index 000000000..87b06c3e9 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs @@ -0,0 +1,38 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using System.Collections.Generic; +using Mapbox.VectorModule.MeshGeneration.GameObjectModifiers; + +[CustomEditor(typeof(PropertiesViewerBehaviour))] +public class PropertiesViewerBehaviourEditor : Editor +{ + public override void OnInspectorGUI() + { + var behaviour = (PropertiesViewerBehaviour)target; + + if (behaviour.Properties == null) + behaviour.Properties = new Dictionary(); + + EditorGUILayout.LabelField("Properties", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + + var keys = new List(behaviour.Properties.Keys); + foreach (var key in keys) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(key, GUILayout.MaxWidth(150)); + string newValue = EditorGUILayout.TextField(behaviour.Properties[key]); + if (newValue != behaviour.Properties[key]) + { + Undo.RecordObject(behaviour, "Edit Property Value"); + behaviour.Properties[key] = newValue; + EditorUtility.SetDirty(behaviour); + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUI.indentLevel--; + } +} +#endif \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta new file mode 100644 index 000000000..f43e371e1 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/PropertiesViewerBehaviourEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ad8df528d29f74fc28f7e7f63b1533e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs new file mode 100644 index 000000000..769544019 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs @@ -0,0 +1,117 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; +using Mapbox.VectorModule.MeshGeneration.Unity; + +[CustomPropertyDrawer(typeof(RoadStyle))] +public class RoadStyleDrawer : PropertyDrawer +{ + private const float LineSpacing = 2f; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + var nameProp = property.FindPropertyRelative("Name"); + var widthProp = property.FindPropertyRelative("Width"); + var pushUpProp = property.FindPropertyRelative("PushUp"); + var materialProp = property.FindPropertyRelative("Material"); + var filtersProp = property.FindPropertyRelative("Filters"); + + // Foldout + Rect foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, nameProp.stringValue, true); + + if (!property.isExpanded) + { + EditorGUI.EndProperty(); + return; + } + + EditorGUI.indentLevel++; + + float y = foldoutRect.y + EditorGUIUtility.singleLineHeight + LineSpacing; + float width = position.width; + + // ClassName + if (nameProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, nameProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Width + if (widthProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, widthProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Width + if (pushUpProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, pushUpProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Material + if (materialProp != null) + { + Rect r = new Rect(position.x, y, width, EditorGUIUtility.singleLineHeight); + EditorGUI.PropertyField(r, materialProp); + y += EditorGUIUtility.singleLineHeight + LineSpacing; + } + + // Filters: delegate to VectorFilterStackDrawer via PropertyField(rect,..., includeChildren: true) + if (filtersProp != null) + { + float filtersHeight = EditorGUI.GetPropertyHeight(filtersProp, true); + Rect r = new Rect(position.x, y, width, filtersHeight); + EditorGUI.PropertyField(r, filtersProp, new GUIContent("Filters"), true); + y += filtersHeight + LineSpacing; + } + + EditorGUI.indentLevel--; + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + float height = EditorGUIUtility.singleLineHeight; // foldout + + if (!property.isExpanded) + return height; + + float spacing = LineSpacing; + var nameProp = property.FindPropertyRelative("Name"); + var widthProp = property.FindPropertyRelative("Width"); + var pushUpProp = property.FindPropertyRelative("PushUp"); + var materialProp = property.FindPropertyRelative("Material"); + var filtersProp = property.FindPropertyRelative("Filters"); + + if (nameProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (widthProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (pushUpProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (materialProp != null) + height += EditorGUIUtility.singleLineHeight + spacing; + + if (filtersProp != null) + { + float filtersHeight = EditorGUI.GetPropertyHeight(filtersProp, true); + height += filtersHeight + spacing; + } + + return height; + } +} +#endif diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta new file mode 100644 index 000000000..8bb5182ac --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43c99405970244b5badb4479094e6702 +timeCreated: 1765443332 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs new file mode 100644 index 000000000..1edb69642 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs @@ -0,0 +1,94 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; + +[CustomEditor(typeof(RoadStyleSheet))] +public class RoadStyleSheetEditor : Editor +{ + private SerializedProperty _stylesProp; + private ReorderableList _stylesList; + + private void OnEnable() + { + _stylesProp = serializedObject.FindProperty("Styles"); + + if (_stylesProp == null) + return; + + _stylesList = new ReorderableList(serializedObject, _stylesProp, true, true, true, true); + + _stylesList.drawHeaderCallback = rect => + { + EditorGUI.LabelField(rect, "Road Styles"); + }; + + _stylesList.elementHeightCallback = index => + { + var element = _stylesProp.GetArrayElementAtIndex(index); + float h = EditorGUI.GetPropertyHeight(element, true); + return h + 4f; + }; + + _stylesList.drawElementCallback = (rect, index, isActive, isFocused) => + { + var element = _stylesProp.GetArrayElementAtIndex(index); + rect.y += 2f; + rect.height = EditorGUI.GetPropertyHeight(element, true); + EditorGUI.PropertyField(rect, element, new GUIContent("Style " + index), true); + }; + + _stylesList.onAddCallback = list => + { + int index = _stylesProp.arraySize; + _stylesProp.InsertArrayElementAtIndex(index); + + var newElement = _stylesProp.GetArrayElementAtIndex(index); + var classNameProp = newElement.FindPropertyRelative("ClassName"); + var widthProp = newElement.FindPropertyRelative("Width"); + var materialProp = newElement.FindPropertyRelative("Material"); + var filtersProp = newElement.FindPropertyRelative("Filters"); + + if (classNameProp != null) classNameProp.stringValue = string.Empty; + if (widthProp != null) widthProp.floatValue = 1f; + if (materialProp != null) materialProp.objectReferenceValue = null; + + // Make sure Filters exists + if (filtersProp != null) + { + var typeProp = filtersProp.FindPropertyRelative("Type"); + var listProp = filtersProp.FindPropertyRelative("Filters"); + + if (typeProp != null) + typeProp.enumValueIndex = 0; // default enum + + if (listProp != null && listProp.isArray) + listProp.arraySize = 0; + } + + serializedObject.ApplyModifiedProperties(); + }; + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.HelpBox( + "Road Style Sheet.\nConfigure styles per road class. Each style has a filter stack.", + MessageType.Info); + + if (_stylesList != null) + { + _stylesList.DoLayoutList(); + } + else + { + EditorGUILayout.PropertyField(_stylesProp, true); + } + + serializedObject.ApplyModifiedProperties(); + } +} +#endif diff --git a/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta new file mode 100644 index 000000000..49ca936d4 --- /dev/null +++ b/Runtime/Mapbox/VectorModule/Editor/RoadStyleSheetEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a5ebee2591fe46999865bf745f99587d +timeCreated: 1765442427 \ No newline at end of file diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs index f71d34c00..55f328752 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/Earcut.cs @@ -1,17 +1,18 @@ using System; using System.Collections.Generic; +using Mapbox.VectorTile.Geometry; using UnityEngine; namespace Mapbox.VectorModule.MeshGeneration { public static class EarcutLibrary { - public static List Earcut(List data, List holeIndices, int dim) + public static List Earcut(float[] data, List holeIndices, int dim) { dim = Math.Max(dim, 2); var hasHoles = holeIndices.Count; - var outerLen = hasHoles > 0 ? holeIndices[0] * dim : data.Count; + var outerLen = hasHoles > 0 ? holeIndices[0] * dim : data.Length; var outerNode = linkedList(data, 0, outerLen, dim, true); var triangles = new List((int)(outerNode.i * 1.5)); @@ -27,7 +28,52 @@ public static List Earcut(List data, List holeIndices, int dim) if (hasHoles > 0) outerNode = EliminateHoles(data, holeIndices, outerNode, dim); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox - if (data.Count > 80 * dim) + if (data.Length > 80 * dim) + { + minX = maxX = data[0]; + minY = maxY = data[1]; + + for (var i = dim; i < outerLen; i += dim) + { + x = data[i]; + y = data[i + 1]; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + // minX, minY and size are later used to transform coords into integers for z-order calculation + size = Math.Max(maxX - minX, maxY - minY); + } + + earcutLinked(outerNode, triangles, dim, minX, minY, size); + + return triangles; + } + + public static List Earcut(float[] data, int[] holeIndices, int dim) + { + dim = Math.Max(dim, 2); + + var hasHoles = holeIndices.Length; + var outerLen = hasHoles > 0 ? holeIndices[0] * dim : data.Length; + var outerNode = linkedList(data, 0, outerLen, dim, true); + var triangles = new List((int)(outerNode.i * 1.5)); + + if (outerNode == null) return triangles; + var minX = 0f; + var minY = 0f; + var maxX = 0f; + var maxY = 0f; + var x = 0f; + var y = 0f; + var size = 0f; + + if (hasHoles > 0) outerNode = EliminateHoles(data, holeIndices, outerNode, dim); + + // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox + if (data.Length > 80 * dim) { minX = maxX = data[0]; minY = maxY = data[1]; @@ -380,7 +426,7 @@ private static Node sortLinked(Node list) return list; } - private static Node EliminateHoles(List data, List holeIndices, Node outerNode, int dim) + private static Node EliminateHoles(float[] data, List holeIndices, Node outerNode, int dim) { var i = 0; var len = holeIndices.Count; @@ -391,16 +437,39 @@ private static Node EliminateHoles(List data, List holeIndices, Node for (i = 0; i < len; i++) { start = holeIndices[i] * dim; - end = i < len - 1 ? holeIndices[i + 1] * dim : data.Count; + end = i < len - 1 ? holeIndices[i + 1] * dim : data.Length; list = linkedList(data, start, end, dim, false); if (list == list.next) list.steiner = true; queue.Add(getLeftmost(list)); } - queue.Sort(delegate (Node a, Node b) + queue.Sort(delegate(Node a, Node b) { return a.x.CompareTo(b.x); }); + + // process holes from left to right + for (i = 0; i < queue.Count; i++) + { + EliminateHole(queue[i], outerNode); + outerNode = FilterPoints(outerNode, outerNode.next); + } + + return outerNode; + } + + private static Node EliminateHoles(float[] data, int[] holeIndices, Node outerNode, int dim) + { + var i = 0; + var len = holeIndices.Length; + var queue = new List(len); + for (i = 0; i < len; i++) { - return (int)Math.Ceiling(a.x - b.x); - }); + var start = holeIndices[i] * dim; + var end = i < len - 1 ? holeIndices[i + 1] * dim : data.Length; + var list = linkedList(data, start, end, dim, false); + if (list == list.next) list.steiner = true; + queue.Add(getLeftmost(list)); + } + + queue.Sort((a, b) => a.x.CompareTo(b.x)); // process holes from left to right for (i = 0; i < queue.Count; i++) @@ -495,9 +564,11 @@ private static Node FindHoleBridge(Node hole, Node outerNode) if (hy == p.y) return p; if (hy == p.next.y) return p.next; } + m = p.x < p.next.x ? p : p.next; } } + p = p.next; } while (p != outerNode); @@ -540,9 +611,9 @@ private static Node FindHoleBridge(Node hole, Node outerNode) private static bool locallyInside(Node a, Node b) { - return area(a.prev, a, a.next) < 0 ? - area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : - area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; + return area(a.prev, a, a.next) < 0 + ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 + : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; } private static float area(Node p, Node q, Node r) @@ -571,7 +642,7 @@ private static Node getLeftmost(Node start) } // create a circular doubly linked list from polygon points in the specified winding order - private static Node linkedList(List data, int start, int end, int dim, bool clockwise) + private static Node linkedList(float[] data, int start, int end, int dim, bool clockwise) { var i = 0; Node last = null; @@ -608,7 +679,7 @@ private static bool equals(Node p1, Node p2) return p1.x == p2.x && p1.y == p2.y; } - private static float signedArea(List data, int start, int end, int dim) + private static float signedArea(float[] data, int start, int end, int dim) { var sum = 0f; var j = end - dim; @@ -649,39 +720,90 @@ public static Data Flatten(List> data) totalVertCount += data[i].Count; } - var result = new Data() { Dim = 2 }; - result.Vertices = new List(totalVertCount * 2); + var result = new Data(); + result.Vertices = new float[totalVertCount * 2]; + result.Holes = new int[dataCount - 1]; var holeIndex = 0; + var index = 0; + var holeNumber = 0; for (var i = 0; i < dataCount; i++) { var subCount = data[i].Count; for (var j = 0; j < subCount; j++) { - result.Vertices.Add(data[i][j][0]); - result.Vertices.Add(data[i][j][2]); + result.Vertices[index++] = (data[i][j][0]); + result.Vertices[index++] = (data[i][j][2]); } + if (i > 0) { holeIndex += data[i - 1].Count; - result.Holes.Add(holeIndex); + result.Holes[holeNumber++] = (holeIndex); } } + + return result; + } + + // public static Data Flatten(MeshVertexData meshData, List subs) + // { + // var dataCount = subs.Count; + // var totalVertCount = 0; + // for (int i = 0; i < dataCount; i++) + // { + // totalVertCount += subs[i].y - subs[i].x; + // } + // + // var result = new Data + // { + // Vertices = new float[totalVertCount * 2], + // Holes = new int[dataCount - 1] + // }; + // var holeIndex = 0; + // + // var index = 0; + // var holeNumber = 0; + // for (var i = 0; i < dataCount; i++) + // { + // var subCount = subs[i].y - subs[i].x; + // for (var j = 0; j < subCount; j++) + // { + // result.Vertices[index++] = (meshData.Vertices[subs[i].x + j][0]); + // result.Vertices[index++] = (meshData.Vertices[subs[i].x + j][2]); + // } + // + // if (i > 0) + // { + // holeIndex = subs[i].x; + // result.Holes[holeNumber++] = (holeIndex); + // } + // } + // + // return result; + // } + + public static Data Flatten(Span meshData) + { + var result = new Data + { + Vertices = new float[meshData.Length * 2] + }; + + for (var i = 0; i < meshData.Length; i++) + { + result.Vertices[i*2 + 0] = meshData[i][0]; + result.Vertices[i*2 + 1] = meshData[i][2]; + } + return result; } } public class Data { - public List Vertices; - public List Holes; - public int Dim; - - public Data() - { - Holes = new List(); - Dim = 2; - } + public float[] Vertices; + public int[] Holes; } public class Node diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs index 58094d2c0..ae0610771 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/ChamferHeightModifier.cs @@ -182,21 +182,24 @@ public void QueryHeight(VectorFeatureUnity feature, MeshData md, out float maxHe { minHeight = 0.0f; maxHeight = 0.0f; - - try + + if (feature.Properties.ContainsKey("height")) { maxHeight = Convert.ToSingle(feature.Properties["height"]); } - catch (Exception) + else { - Debug.LogError("Property: '" + "height" + "' must contain a numerical value for extrusion."); - return; + maxHeight = 0; } if (feature.Properties.ContainsKey("min_height")) { minHeight = Convert.ToSingle(feature.Properties["min_height"]); } + else + { + minHeight = 0; + } } public void Chamfer(VectorFeatureUnity feature, MeshData md) diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs index 6e65b8358..9d5553ef7 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/PolygonMeshModifier.cs @@ -77,7 +77,7 @@ private void Polygonize(VectorFeatureUnity feature, MeshData md, float tileSize, if (IsClockwise(sub) && vertCount > 0) { flatData = EarcutLibrary.Flatten(subset); - result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, flatData.Dim); + result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, 2); polygonVertexCount = result.Count; if (triList == null) { @@ -127,7 +127,7 @@ private void Polygonize(VectorFeatureUnity feature, MeshData md, float tileSize, } flatData = EarcutLibrary.Flatten(subset); - result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, flatData.Dim); + result = EarcutLibrary.Earcut(flatData.Vertices, flatData.Holes, 2); polygonVertexCount = result.Count; // if (_options.texturingType == UvMapType.Atlas || _options.texturingType == UvMapType.AtlasWithColorPalette) diff --git a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs index e4838773d..bf08231e3 100644 --- a/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs +++ b/Runtime/Mapbox/VectorModule/MeshGeneration/MeshModifiers/SnapTerrainModifier.cs @@ -25,7 +25,7 @@ public class SnapTerrainModifierCore { public void Run(VectorFeatureUnity feature, MeshData md, IMapInformation mapInformation) { - var rectd = Conversions.TileBoundsInUnitySpace(feature.TileId, mapInformation.CenterMercator, mapInformation.Scale); + var tileSize = Conversions.TileSizeInUnitySpace(feature.TileId.Z, mapInformation.Scale); var _counter = md.Vertices.Count; if (_counter > 0) { @@ -33,10 +33,10 @@ public void Run(VectorFeatureUnity feature, MeshData md, IMapInformation mapInfo { var h = mapInformation.QueryElevation( feature.TileId, - (float)(md.Vertices[i].x) + .5f, - (float)(md.Vertices[i].z) + .5f) + (md.Vertices[i].x) + .5f, + (md.Vertices[i].z) + .5f) / mapInformation.Scale - / rectd.Size.x; + / tileSize; md.Vertices[i] += new Vector3(0, (float) h, 0); } } @@ -50,11 +50,11 @@ public void Run(VectorFeatureUnity feature, MeshData md, IMapInformation mapInfo var h = 0f; if (mapInformation.QueryElevation != null) { - h = (float)(mapInformation.QueryElevation( + h = (mapInformation.QueryElevation( feature.TileId, - (float) (sub[i].x), - (float) (sub[i].z + 1)) / mapInformation.Scale - / rectd.Size.x); + (sub[i].x), + (sub[i].z + 1)) / mapInformation.Scale + / tileSize); } sub[i] += new Vector3(0, h, 0); diff --git a/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs b/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs index 5fdf44af8..9c4ceb031 100644 --- a/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs +++ b/Runtime/Mapbox/VectorModule/Unity/VectorLayerModuleScript.cs @@ -9,9 +9,9 @@ namespace Mapbox.VectorModule.Unity { public class VectorLayerModuleScript : ModuleConstructorScript { - [SerializeField] private VectorModuleSettings vectorModuleSettings; + [SerializeField] protected VectorModuleSettings vectorModuleSettings; - [SerializeField] private List _layerVisualizers; + [SerializeField] private List _layerVisualizers; public override ILayerModule ModuleImplementation { get; protected set; } public void Start() @@ -21,19 +21,20 @@ public void Start() public override ILayerModule ConstructModule(MapService service, IMapInformation mapInformation, UnityContext unityContext) { - var dictionary = new Dictionary(); + var dictionary = new Dictionary>(); foreach (var visualizerObject in _layerVisualizers) { if(visualizerObject == null) continue; var visualizer = visualizerObject.ConstructLayerVisualizer(mapInformation, unityContext); - dictionary.Add(visualizer.VectorLayerName, visualizer); + if(!dictionary.ContainsKey(visualizer.VectorLayerName)) + dictionary.Add(visualizer.VectorLayerName, new List()); + dictionary[visualizer.VectorLayerName].Add(visualizer); } ModuleImplementation = GetVectorLayerModule(mapInformation, unityContext, service, dictionary); return ModuleImplementation; } - private VectorLayerModule GetVectorLayerModule(IMapInformation mapInformation, UnityContext unityContext, - MapService service, Dictionary dictionary) + protected virtual VectorLayerModule GetVectorLayerModule(IMapInformation mapInformation, UnityContext unityContext, MapService service, Dictionary> dictionary) { if (vectorModuleSettings.SourceType != VectorSourceType.Custom) { diff --git a/Runtime/Mapbox/VectorModule/VectorLayerModule.cs b/Runtime/Mapbox/VectorModule/VectorLayerModule.cs index 5e874e5a1..887990fc3 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerModule.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerModule.cs @@ -10,6 +10,7 @@ using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; using Mapbox.VectorModule.MeshGeneration; +using Mapbox.VectorTile.ExtensionMethods; using UnityEngine; using Console = System.Console; @@ -18,10 +19,10 @@ namespace Mapbox.VectorModule public class VectorLayerModule : ILayerModule { //mesh gen - private bool _isActive = true; - private UnityContext _unityContext; - private Dictionary _activeTasks; - private Dictionary _layerVisualizers; + protected bool _isActive = true; + protected UnityContext _unityContext; + protected Dictionary> _activeTasks; + protected Dictionary> _layerVisualizers; private Source _vectorSource; private VectorModuleSettings _vectorModuleSettings; @@ -32,26 +33,37 @@ public class VectorLayerModule : ILayerModule private HashSet _readyTiles; private List _tilesToRemove; - public VectorLayerModule(IMapInformation mapInformation, Source source, UnityContext unityContext, Dictionary layerVisualizers, VectorModuleSettings vectorModuleSettings = null) : base() + public VectorLayerModule(IMapInformation mapInformation, Source source, UnityContext unityContext, Dictionary> layerVisualizers, VectorModuleSettings vectorModuleSettings = null) : base() { _unityContext = unityContext; _layerVisualizers = layerVisualizers; _mapInformation = mapInformation; + _vectorSource = source; + if (_vectorSource != null) + { + _vectorSource.CacheItemDisposed += ClearDisposedDataVisual; + } + _vectorModuleSettings = vectorModuleSettings ?? new VectorModuleSettings(); _readyTiles = new HashSet(); - _vectorSource.CacheItemDisposed += ClearDisposedDataVisual; _retainedTiles = new HashSet(); - _activeTasks = new Dictionary(); + _activeTasks = new Dictionary>(); _tilesToRemove = new List(10); } public virtual IEnumerator Initialize() { - yield return _vectorSource.Initialize(); - foreach (var visualizer in _layerVisualizers.Values) + if (_vectorSource != null) { - yield return visualizer.Initialize(); + yield return _vectorSource.Initialize(); + } + foreach (var visualizers in _layerVisualizers.Values) + { + foreach (var visualizer in visualizers) + { + yield return visualizer.Initialize(); + } } } @@ -64,23 +76,34 @@ public virtual bool LoadInstant(UnityMapTile unityTile) { var targetId = GetTargetTileId(unityTile.CanonicalTileId); if (_readyTiles.Contains(targetId)) - return true; - - //Debug.Log(string.Format("Load Instant {0}, {1}, {2}" ,unityTile.CanonicalTileId, _vectorSource.CheckInstantData(unityTile.CanonicalTileId), _visualCache.ContainsKey(unityTile.CanonicalTileId))); - if (!IsZinSupportedRange(targetId.Z)) return true; - - //this is wrong, it feels wrong - //tile doesn't need data, only yhe visual object. why are we checking for data - if (_vectorSource.GetInstantData(targetId, out var instantData) && - unityTile.TerrainContainer.State == TileContainerState.Final) { - if(!IsMeshGenInWork(targetId)) + foreach (var pair in _layerVisualizers) { - CreateVisual(targetId, instantData); + foreach (var visualizer in pair.Value) + { + visualizer.SetActive(targetId, true, _mapInformation); + } } + return true; } + else + { + //Debug.Log(string.Format("Load Instant {0}, {1}, {2}" ,unityTile.CanonicalTileId, _vectorSource.CheckInstantData(unityTile.CanonicalTileId), _visualCache.ContainsKey(unityTile.CanonicalTileId))); + if (!IsZinSupportedRange(targetId.Z)) return true; - return false; + //this is wrong, it feels wrong + //tile doesn't need data, only yhe visual object. why are we checking for data + if (_vectorSource.GetInstantData(targetId, out var instantData) && + unityTile.TerrainContainer.State == TileContainerState.Final) + { + if (!IsMeshGenInWork(targetId)) + { + CreateVisual(targetId, instantData); + } + } + + return false; + } } public virtual bool RetainTiles(HashSet retainedTiles) @@ -91,18 +114,24 @@ public virtual bool RetainTiles(HashSet retainedTiles) foreach (var tileId in _readyTiles) { var isActive = _retainedTiles.Contains(tileId); - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.SetActive(tileId, isActive, _mapInformation); + foreach (var visualizer in pair.Value) + { + visualizer.SetActive(tileId, isActive, _mapInformation); + } } if (!isActive) { _tilesToRemove.Add(tileId); - if (_activeTasks.TryGetValue(tileId, out var task)) + if (_activeTasks.TryGetValue(tileId, out var tasks)) { - _activeTasks.Remove(tileId); - task.Cancel(); + foreach (var task in tasks) + { + _activeTasks.Remove(tileId); + task.Cancel(); + } } } } @@ -115,10 +144,15 @@ public virtual bool RetainTiles(HashSet retainedTiles) //cancel tasks for tiles we no longer need //this prevents flickers from buildings appearing in temp tiles //while we are waiting for the final real tile - foreach (var task in _activeTasks) + foreach (var taskPair in _activeTasks) { - if(!_retainedTiles.Contains(task.Key)) - task.Value.Cancel(); + if (!_retainedTiles.Contains(taskPair.Key)) + { + foreach (var task in taskPair.Value) + { + task.Cancel(); + } + } } var isReady = _vectorSource.RetainTiles(_retainedTiles); @@ -140,9 +174,12 @@ public virtual void UpdatePositioning(IMapInformation information) public virtual void OnDestroy() { _isActive = false; - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.OnDestroy(); + foreach (var visualizer in pair.Value) + { + visualizer.OnDestroy(); + } } } @@ -163,7 +200,14 @@ public IEnumerable GetReadyTiles() public bool TryGetLayerVisualizer(string name, out IVectorLayerVisualizer visualizer) { - return _layerVisualizers.TryGetValue(name, out visualizer); + if (_layerVisualizers.TryGetValue(name, out var visualizers)) + { + visualizer = visualizers.FirstOrDefault(); + return true; + } + + visualizer = null; + return false; } public IEnumerable GetDataId(IEnumerable tileIdList) @@ -227,7 +271,7 @@ public virtual IEnumerator LoadTiles(IEnumerable tiles) public IEnumerable GetTileCoverCoroutines(IEnumerable tiles) { var targetTiles = tiles.Where(x => IsZinSupportedRange(x.Z)).Select(GetTargetTileId).Distinct(); - return targetTiles.Select(x => LoadAndProcessTileCoroutine(x)); + return targetTiles.Select(LoadAndProcessTileCoroutine); } #endregion @@ -362,14 +406,20 @@ private IEnumerator CreateVisualCoroutine(CanonicalTileId tileId, VectorData vec private void ClearDisposedDataVisual(CanonicalTileId tileId) { - if (_activeTasks.TryGetValue(tileId, out var task)) + if (_activeTasks.TryGetValue(tileId, out var tasks)) { - task.Cancel(); + foreach (var task in tasks) + { + task.Cancel(); + } } _readyTiles.Remove(tileId); - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.UnregisterTile(tileId); + foreach (var visualizer in pair.Value) + { + visualizer.UnregisterTile(tileId); + } } OnVectorMeshDestroyed(tileId); @@ -379,20 +429,24 @@ private void ClearDisposedDataVisual(CanonicalTileId tileId) private void UpdateForView(CanonicalTileId tileId, IMapInformation information) { - foreach (var visualizer in _layerVisualizers) + foreach (var pair in _layerVisualizers) { - visualizer.Value.UpdateForView(tileId, information); + foreach (var visualizer in pair.Value) + { + visualizer.UpdateForView(tileId, information); + } } } - private void MeshGeneration(VectorData data, Action callback) + protected virtual void MeshGeneration(VectorData data, Action callback) { if (data.Data == null) { callback(new MeshGenerationTaskResult(TaskResultType.Success)); + return; } - var meshTask = new MeshGenTaskWrapper() + var meshTask = new MeshGenTaskWrapper() { TileId = data.TileId, DataAction = () => @@ -415,14 +469,19 @@ private void MeshGeneration(VectorData data, Action ca var layers = data.VectorTileData.LayerNames(); foreach (var layerName in layers) { - if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizer)) + if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizers)) { - if(layerVisualizer.ContainsVisualFor(data.TileId)) - continue; - if (layerVisualizer.Active) - { - result.Data.Add(layerName, layerVisualizer.CreateMesh(data.TileId, data.VectorTileData.GetLayer(layerName))); - } + for (var vi = 0; vi < layerVisualizers.Count; vi++) + { + var layerVisualizer = layerVisualizers[vi]; + if (layerVisualizer.ContainsVisualFor(data.TileId)) + continue; + if (layerVisualizer.Active) + { + var key = layerVisualizers.Count > 1 ? $"{layerName}_{vi}" : layerName; + result.Data.Add(key, layerVisualizer.CreateMesh(data.TileId, data.VectorTileData.GetLayer(layerName))); + } + } } } } @@ -467,18 +526,21 @@ private void MeshGeneration(VectorData data, Action ca var resultGameObjects = new List(); foreach (var layerName in data.VectorTileData.LayerNames()) { - if (!taskResult.Data.ContainsKey(layerName)) - continue; - - if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizer)) + if (_layerVisualizers.TryGetValue(layerName, out var layerVisualizers)) { - var tileMeshData = taskResult.Data[layerName]; - var layerGameObjects = layerVisualizer.CreateGo(data.TileId, tileMeshData); - foreach (var gameObject in layerGameObjects) - { - gameObject.SetActive(false); - resultGameObjects.Add(gameObject); - } + for (var vi = 0; vi < layerVisualizers.Count; vi++) + { + var key = layerVisualizers.Count > 1 ? $"{layerName}_{vi}" : layerName; + if (!taskResult.Data.TryGetValue(key, out var tileMeshData)) + continue; + var layerVisualizer = layerVisualizers[vi]; + var layerGameObjects = layerVisualizer.CreateGo(data.TileId, tileMeshData); + foreach (var gameObject in layerGameObjects) + { + gameObject.SetActive(false); + resultGameObjects.Add(gameObject); + } + } } } callback(new MeshGenerationTaskResult(TaskResultType.Success, resultGameObjects)); @@ -486,7 +548,8 @@ private void MeshGeneration(VectorData data, Action ca } }; - _activeTasks.Add(data.TileId, meshTask); + if(!_activeTasks.ContainsKey(data.TileId)) _activeTasks.Add(data.TileId, new List()); + _activeTasks[data.TileId].Add(meshTask); _unityContext.TaskManager.AddTask(meshTask, 0); } diff --git a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs index 6b551cf20..24129fcb9 100644 --- a/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs +++ b/Runtime/Mapbox/VectorModule/VectorLayerVisualizer.cs @@ -44,10 +44,18 @@ public VectorLayerVisualizer(string name, IMapInformation mapInformation, UnityC _layerRootObject = new GameObject(_vectorLayerName + " layer objects").transform; if (_unityContext != null) { - _layerRootObject.SetParent(_unityContext.RuntimeGenerationRoot); + // worldPositionStays: false so Unity doesn't counter-rotate localRotation + // when parenting under a rotated RuntimeGenerationRoot (e.g. MapRoot + // anchored to an AR rig). The layer root should compose with the parent + // chain, not pin to world space. + _layerRootObject.SetParent(_unityContext.RuntimeGenerationRoot, worldPositionStays: false); } _layerRootObject.transform.localPosition = Vector3.zero; - _layerRootObject.transform.position += _settings.Offset; + _layerRootObject.transform.localRotation = Quaternion.identity; + // localPosition (not position) so the offset stays in MapRoot-local space. + // Writing world-space here would clobber any parent transform on the map + // hierarchy (e.g. an AR anchor rotating / translating MapRoot). + _layerRootObject.transform.localPosition += _settings.Offset; } public virtual void UpdateForView(CanonicalTileId canonicalTileId, IMapInformation information) @@ -57,10 +65,13 @@ public virtual void UpdateForView(CanonicalTileId canonicalTileId, IMapInformati foreach (var entity in visuals) { _mapInformation.PositionObjectFor(canonicalTileId, out var position, out var scale); + // Both operands in MapRoot-local space: `position` comes from + // PositionObjectFor (Mercator-derived, map-local), and we read the + // layer root's localPosition rather than its world-space position. entity.GameObject.transform.localPosition = new Vector3( - position.x - _layerRootObject.transform.position.x, - entity.GameObject.transform.localPosition.y, - position.z - _layerRootObject.transform.position.z); + position.x - _layerRootObject.transform.localPosition.x, + entity.GameObject.transform.localPosition.y, + position.z - _layerRootObject.transform.localPosition.z); var stack = _stackList[entity.StackId]; if (stack.Settings.LayerType == LayerTypeEnum.Polygon || stack.Settings.LayerType == LayerTypeEnum.Line) @@ -84,6 +95,10 @@ public virtual void SetActive(CanonicalTileId canonicalTileId, bool isActive, IM var isVisible = stack.IsZinSupportedRange(_mapInformation.AbsoluteZoom); entity.GameObject.SetActive(isVisible); } + else + { + Debug.Log("All entities should have a stack id"); + } } } } @@ -157,6 +172,25 @@ public virtual void UnregisterTile(CanonicalTileId tileId) modifierStack.UnregisterTile(tileId); } } + + public virtual void ClearCaches() + { + foreach (var entities in _results.Values) + { + foreach (var entity in entities) + { + OnVectorMeshDestroyed(entity.GameObject); + GameObject.Destroy(entity.GameObject); + } + } + + foreach (var stack in _stackList) + { + stack.Value.OnDestroy(); + } + + _results.Clear(); + } public void OnDestroy() { @@ -225,7 +259,7 @@ protected List GameObjectModifications(CanonicalTileId canonicalTile foreach (var meshData in pair.Value) { var entity = _stackList[pair.Key].CreateEntity(meshData); - entity.GameObject.transform.SetParent(_layerRootObject); + entity.GameObject.transform.SetParent(_layerRootObject, worldPositionStays: false); entity.StackId = pair.Key; entity.Feature = meshData.Feature; if(Application.isEditor) entity.GameObject.name = VectorLayerName + " " + canonicalTileId.ToString(); diff --git a/Samples~/.DS_Store b/Samples~/.DS_Store index e74ac8166..5722b7afe 100644 Binary files a/Samples~/.DS_Store and b/Samples~/.DS_Store differ diff --git a/Samples~/LocationBasedGame/.DS_Store b/Samples~/LocationBasedGame/.DS_Store index 3c24ee149..df8238fd8 100644 Binary files a/Samples~/LocationBasedGame/.DS_Store and b/Samples~/LocationBasedGame/.DS_Store differ diff --git a/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs b/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs index c37b9244a..29fbcc666 100644 --- a/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs +++ b/Samples~/LocationBasedGame/CharacterVisuals/Astronaut/CharacterMovement.cs @@ -11,6 +11,7 @@ namespace Mapbox.Examples public class CharacterMovement : MonoBehaviour { public MapBehaviourCore MapBehaviour; + private MapboxMap _map; private IMapInformation _mapInformation; public Transform Target; public Animator CharacterAnimator; @@ -24,6 +25,7 @@ private void Start() { MapBehaviour.Initialized += map => { + _map = map; _mapInformation = map.MapInformation; _scale = map.MapInformation.Scale; _readyForUpdates = true; @@ -42,6 +44,7 @@ void Update() transform.LookAt(transform.position + direction); transform.Translate(Vector3.forward * (Speed/_scale)); if(CharacterAnimator) CharacterAnimator.SetBool("IsWalking", true); + _map.ForceRedraw(); } else { diff --git a/Samples~/LocationBasedGame/LocationGame.unity b/Samples~/LocationBasedGame/LocationGame.unity index 537b04d85..d09f609d8 100644 --- a/Samples~/LocationBasedGame/LocationGame.unity +++ b/Samples~/LocationBasedGame/LocationGame.unity @@ -142,7 +142,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &6544153 RectTransform: m_ObjectHideFlags: 0 @@ -161,7 +161,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 0.5} m_AnchorMax: {x: 0, y: 0.5} m_AnchoredPosition: {x: 16, y: -62} - m_SizeDelta: {x: 577.9446, y: 0} + m_SizeDelta: {x: 577.9446, y: 563} m_Pivot: {x: 0, y: 0.5} --- !u!114 &6544154 MonoBehaviour: @@ -365,11 +365,46 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!4 &220147485 stripped +--- !u!1 &220147481 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 220147485} + - component: {fileID: 220147484} + m_Layer: 0 + m_Name: CharTarget + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!33 &220147484 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 220147481} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &220147485 Transform: - m_CorrespondingSourceObject: {fileID: 7512784583129558687, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - m_PrefabInstance: {fileID: 7512784582910460290} + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 220147481} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0.40000007} + m_LocalScale: {x: 0.2, y: 0.19999996, z: 0.19999996} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &482478434 GameObject: m_ObjectHideFlags: 0 @@ -445,6 +480,95 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 482478434} m_CullTransparentMesh: 1 +--- !u!1 &525849124 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 525849129} + - component: {fileID: 525849128} + - component: {fileID: 525849131} + - component: {fileID: 525849130} + m_Layer: 0 + m_Name: LocationModule + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &525849128 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b55f37f9a6f7e44f7bb35e6bc3863847, type: 3} + m_Name: + m_EditorClassIdentifier: + LocationProviderType: 1 + _mapboxLocationProviderSettings: + AccuracyLevel: 0 + Displacement: 0 + MinimumInterval: 0 + MaximumInterval: 0 + Interval: 0 + _unityLocationProviderSettings: + DesiredAccuracyInMeters: 1 + UpdateDistanceInMeters: 0 + UpdateTimeInMilliSeconds: 500 + EditorLatitudeLongitude: 60.1712373,24.9435977 + _dontDestroyOnLoad: 0 +--- !u!4 &525849129 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0.40000007} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &525849130 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a5534c9989da84262bc64ffe27afbbb6, type: 3} + m_Name: + m_EditorClassIdentifier: + Map: {fileID: 1316629399} + LocationProviderFactory: {fileID: 525849128} + ShiftMapDistance: 500 +--- !u!114 &525849131 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 137870786121148788eada666e6cd719, type: 3} + m_Name: + m_EditorClassIdentifier: + _transform: {fileID: 220147485} + _locationProvider: {fileID: 525849128} + _mapCore: {fileID: 1316629399} --- !u!1 &545031483 GameObject: m_ObjectHideFlags: 0 @@ -826,6 +950,112 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} +--- !u!1 &885811049 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 885811053} + - component: {fileID: 885811052} + - component: {fileID: 885811051} + - component: {fileID: 885811050} + m_Layer: 0 + m_Name: Plane + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!64 &885811050 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 5 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &885811051 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + m_Enabled: 0 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &885811052 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &885811053 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 885811049} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0.40000007} + m_LocalScale: {x: 999, y: 998.99976, z: 998.99976} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &923185051 GameObject: m_ObjectHideFlags: 0 @@ -862,7 +1092,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 288.9723, y: -281.5} - m_SizeDelta: {x: 545.9446, y: 0} + m_SizeDelta: {x: 545.9446, y: 531} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &923185053 MonoBehaviour: @@ -1370,17 +1600,115 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1271358336} m_CullTransparentMesh: 1 ---- !u!114 &1316629399 stripped +--- !u!1 &1316629397 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1316629400} + - component: {fileID: 1316629399} + - component: {fileID: 1377339556045437849} + - component: {fileID: 1316629404} + - component: {fileID: 1316629402} + m_Layer: 0 + m_Name: Map + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1316629399 MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 7512784584146348565, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - m_PrefabInstance: {fileID: 7512784582910460290} + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} + m_GameObject: {fileID: 1316629397} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 8987c930e6fb0479a907044faef4ab26, type: 3} m_Name: m_EditorClassIdentifier: + MapInformation: + _pitch: 45 + _bearing: 0 + _scale: 1 + _latitudeLongitudeString: 60.1712373,24.9435977 + _zoom: 15 + UnityContext: + LocationPermissionState: 0 + MapRoot: {fileID: 1316629400} + BaseTileRoot: {fileID: 0} + RuntimeGenerationRoot: {fileID: 0} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} + TileProvider: {fileID: 8067237297740704311} + DataFetcher: {fileID: 0} + CacheManager: {fileID: 0} + LocationFactory: {fileID: 525849128} + InitializeOnStart: 1 +--- !u!4 &1316629400 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2054936215} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1316629402 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 13ef846e428d4d41b3827620e39b8a6a, type: 3} + m_Name: + m_EditorClassIdentifier: + vectorModuleSettings: + SourceType: 0 + CustomSourceId: + DataSettings: + CacheSize: 100 + ClampDataLevelToMax: 15 + RejectTilesOutsideZoom: {x: 12, y: 16} + _layerVisualizers: + - {fileID: 11400000, guid: 4aa8336b8547a4f4fa3bff4d3fb0c106, type: 2} +--- !u!114 &1316629404 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ac0bc0b01b604b8889bb66087e8c94d9, type: 3} + m_Name: + m_EditorClassIdentifier: + Settings: + SourceType: 0 + CustomSourceId: + LoadBackgroundTextures: 0 + DataSettings: + UseRetinaTextures: 1 + UseNonReadableTextures: 1 + CacheSize: 100 + ClampDataLevelToMax: 25 + RejectTilesOutsideZoom: {x: 2, y: 25} --- !u!1 &1326738087 GameObject: m_ObjectHideFlags: 0 @@ -2128,6 +2456,41 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1815118998} m_Mesh: {fileID: 10210, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &2054936213 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054936215} + m_Layer: 0 + m_Name: MapUtility + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2054936215 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054936213} + serializedVersion: 2 + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0.4, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 525849129} + - {fileID: 220147485} + - {fileID: 885811053} + - {fileID: 8694687542404769455} + m_Father: {fileID: 1316629400} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &2117726497 GameObject: m_ObjectHideFlags: 0 @@ -3217,86 +3580,77 @@ SkinnedMeshRenderer: m_Center: {x: -0.0038077123, y: 0.003856523, z: 0.0011322275} m_Extent: {x: 0.049188334, y: 0.023567023, z: 0.03146985} m_DirtyAABB: 0 ---- !u!1001 &7512784582910460290 -PrefabInstance: +--- !u!114 &1377339556045437849 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2fa13573d971f40649081b02ffc8366c, type: 3} + m_Name: + m_EditorClassIdentifier: + TileMaterials: + - {fileID: -876546973899608171, guid: e44ed2d518a7c4a20bd9799360a9e7ed, type: 3} + CacheSize: 25 +--- !u!1 &6450514213955001649 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8694687542404769455} + - component: {fileID: 8067237297740704311} + m_Layer: 0 + m_Name: TileProvider + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &8067237297740704311 +MonoBehaviour: m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6450514213955001649} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 63b76f2761e454a4687f320fa7524dde, type: 3} + m_Name: + m_EditorClassIdentifier: + TileProvider: + Transform: {fileID: 4601729577895354} + TilesToWest: 2 + TilesToEast: 2 + TilesToNorth: 2 + TilesToSouth: 2 +--- !u!4 &8694687542404769455 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6450514213955001649} serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: 555719919710579637, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: TileProvider.Transform - value: - objectReference: {fileID: 4601729577895354} - - target: {fileID: 7512784584146348565, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: _tileCreatorBehaviour - value: - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348567, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_Name - value: Map - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348568, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: vectorModuleSettings.DataSettings.ClampDataLevelToMax - value: 15 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_RootOrder - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalPosition.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalPosition.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalRotation.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7512784584146348570, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - m_RemovedComponents: - - {fileID: 7512784584146348569, guid: 5cfb21e958bce467e966040b1be49240, type: 3} - m_RemovedGameObjects: [] - m_AddedGameObjects: [] - m_AddedComponents: [] - m_SourcePrefab: {fileID: 100100000, guid: 5cfb21e958bce467e966040b1be49240, type: 3} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2054936215} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 745711776} - - {fileID: 7512784582910460290} + - {fileID: 1316629400} - {fileID: 166616397} - {fileID: 1326738089} - {fileID: 4601729577895354} diff --git a/Samples~/LocationBasedGame/LocationGameWithPois.unity b/Samples~/LocationBasedGame/LocationGameWithPois.unity index 897b2787a..f9af243de 100644 --- a/Samples~/LocationBasedGame/LocationGameWithPois.unity +++ b/Samples~/LocationBasedGame/LocationGameWithPois.unity @@ -490,7 +490,7 @@ GameObject: m_Component: - component: {fileID: 525849129} - component: {fileID: 525849128} - - component: {fileID: 525849127} + - component: {fileID: 525849131} - component: {fileID: 525849130} m_Layer: 0 m_Name: LocationModule @@ -499,26 +499,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &525849127 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 525849124} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 0a38712e93231418a84665190b8473d0, type: 3} - m_Name: - m_EditorClassIdentifier: - _desiredAccuracyInMeters: 5 - _updateDistanceInMeters: 5 - _updateTimeInMilliSeconds: 1000 - _userHeadingSmoothing: {fileID: 0} - _deviceOrientationSmoothing: {fileID: 0} - _editorDebuggingOnly: - _mockUnityInputLocation: 0 - _locationLogFile: {fileID: 0} --- !u!114 &525849128 MonoBehaviour: m_ObjectHideFlags: 0 @@ -531,10 +511,18 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: b55f37f9a6f7e44f7bb35e6bc3863847, type: 3} m_Name: m_EditorClassIdentifier: - _deviceLocationProviderUnity: {fileID: 525849127} - _deviceLocationProviderAndroid: {fileID: 0} - _editorLocationProvider: {fileID: 525849130} - _transformLocationProvider: {fileID: 0} + LocationProviderType: 1 + _mapboxLocationProviderSettings: + AccuracyLevel: 0 + Displacement: 0 + MinimumInterval: 0 + MaximumInterval: 0 + Interval: 0 + _unityLocationProviderSettings: + DesiredAccuracyInMeters: 1 + UpdateDistanceInMeters: 0 + UpdateTimeInMilliSeconds: 500 + EditorLatitudeLongitude: 60.1712373,24.9435977 _dontDestroyOnLoad: 0 --- !u!4 &525849129 Transform: @@ -560,14 +548,27 @@ MonoBehaviour: m_GameObject: {fileID: 525849124} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 60712efc3153a4819b0c79437175846d, type: 3} + m_Script: {fileID: 11500000, guid: a5534c9989da84262bc64ffe27afbbb6, type: 3} + m_Name: + m_EditorClassIdentifier: + Map: {fileID: 1316629399} + LocationProviderFactory: {fileID: 525849128} + ShiftMapDistance: 500 +--- !u!114 &525849131 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 525849124} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 137870786121148788eada666e6cd719, type: 3} m_Name: m_EditorClassIdentifier: - _accuracy: 0 - _autoFireEvent: 1 - _updateInterval: 0 - _sendEvent: 0 - _latitudeLongitude: 60.1712373,24.9435977 + _transform: {fileID: 220147485} + _locationProvider: {fileID: 525849128} + _mapCore: {fileID: 1316629399} --- !u!1 &545031483 GameObject: m_ObjectHideFlags: 0 @@ -1609,10 +1610,9 @@ GameObject: m_Component: - component: {fileID: 1316629400} - component: {fileID: 1316629399} + - component: {fileID: 1316629405} - component: {fileID: 1316629404} - component: {fileID: 1316629402} - - component: {fileID: 7512784582834766863} - - component: {fileID: 1377339556045437848} m_Layer: 0 m_Name: Map m_TagString: Untagged @@ -1639,14 +1639,17 @@ MonoBehaviour: _latitudeLongitudeString: 60.1712373,24.9435977 _zoom: 15 UnityContext: + LocationPermissionState: 0 MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 0} RuntimeGenerationRoot: {fileID: 0} _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 8067237297740704311} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} - InitializeOnStart: 0 + LocationFactory: {fileID: 525849128} + InitializeOnStart: 1 --- !u!4 &1316629400 Transform: m_ObjectHideFlags: 0 @@ -1707,6 +1710,21 @@ MonoBehaviour: CacheSize: 100 ClampDataLevelToMax: 25 RejectTilesOutsideZoom: {x: 2, y: 25} +--- !u!114 &1316629405 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2fa13573d971f40649081b02ffc8366c, type: 3} + m_Name: + m_EditorClassIdentifier: + TileMaterials: + - {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} + CacheSize: 25 --- !u!1 &1326738087 GameObject: m_ObjectHideFlags: 0 @@ -3578,21 +3596,6 @@ SkinnedMeshRenderer: m_Center: {x: -0.0038077123, y: 0.003856523, z: 0.0011322275} m_Extent: {x: 0.049188334, y: 0.023567023, z: 0.03146985} m_DirtyAABB: 0 ---- !u!114 &1377339556045437848 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1316629397} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 137870786121148788eada666e6cd719, type: 3} - m_Name: - m_EditorClassIdentifier: - _transform: {fileID: 220147485} - _locationProvider: {fileID: 525849128} - _mapCore: {fileID: 1316629399} --- !u!1 &6450514213955001649 GameObject: m_ObjectHideFlags: 0 @@ -3610,22 +3613,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &7512784582834766863 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1316629397} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 46cd4a863e31244f891143a5bab077bc, type: 3} - m_Name: - m_EditorClassIdentifier: - InitializeMap: 1 - ContinueAfterInitialization: 0 - _locationProvider: {fileID: 525849128} - _map: {fileID: 1316629399} --- !u!114 &8067237297740704311 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs b/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs deleted file mode 100644 index 0013af629..000000000 --- a/Samples~/LocationBasedGame/Scripts/SnapMapToLocationProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Mapbox.BaseModule.Map; -using Mapbox.Example.Scripts.Map; -using Mapbox.LocationModule; -using UnityEngine; - -namespace Mapbox.Example.Scripts.LocationBehaviours -{ - public class SnapMapToLocationProvider : MonoBehaviour - { - public bool InitializeMap = true; - public bool ContinueAfterInitialization = false; - [SerializeField] - private LocationProviderFactory _locationProvider; - - [SerializeField] private MapboxMapBehaviour _map; - private bool _initializeStarted = false; - - private void Start() - { - - if(_locationProvider == null) - Debug.Log("_locationProvider null"); - - if(_locationProvider.DefaultLocationProvider == null) - Debug.Log("DefaultLocationProvider null"); - - if(!enabled || _locationProvider == null || _locationProvider.DefaultLocationProvider == null) - return; - - UnityEngine.Input.location.Start(); - _locationProvider.DefaultLocationProvider.OnLocationUpdated += (s) => - { - if (_map.InitializationStatus == InitializationStatus.WaitingForInitialization && InitializeMap && !_initializeStarted) - { - _initializeStarted = true; - _map.MapInformation.Initialize(s.LatitudeLongitude); - _map.Initialize(); - } - if (ContinueAfterInitialization) - { - _map.MapInformation.SetInformation(s.LatitudeLongitude); - } - - }; - } - } -} diff --git a/Samples~/MapboxComponents/Areas.meta b/Samples~/MapboxComponents/Areas.meta new file mode 100644 index 000000000..9dbc7df2e --- /dev/null +++ b/Samples~/MapboxComponents/Areas.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6e0eececb267c4f68831fccfa80a1877 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Commercial.mat b/Samples~/MapboxComponents/Areas/Commercial.mat new file mode 100644 index 000000000..9b292f846 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Commercial.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Commercial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.96862745, g: 0.9490196, b: 0.8901961, a: 1} + - _Color: {r: 0.9686274, g: 0.9490196, b: 0.8901961, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Commercial.mat.meta b/Samples~/MapboxComponents/Areas/Commercial.mat.meta new file mode 100644 index 000000000..3272c0537 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Commercial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a64b34cd91bab49cf8f4d08438453d85 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Grass.mat b/Samples~/MapboxComponents/Areas/Grass.mat new file mode 100644 index 000000000..a9d6d24dd --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Grass.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Grass + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8352941, g: 0.94509804, b: 0.8156863, a: 1} + - _Color: {r: 0.8352941, g: 0.945098, b: 0.8156863, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Grass.mat.meta b/Samples~/MapboxComponents/Areas/Grass.mat.meta new file mode 100644 index 000000000..1b2d08274 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Grass.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a7105d373b04b4d2089924fdba9f19b0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Park.mat b/Samples~/MapboxComponents/Areas/Park.mat new file mode 100644 index 000000000..7b3f2b908 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Park.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Park + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.72156864, g: 0.92156863, b: 0.6784314, a: 1} + - _Color: {r: 0.72156864, g: 0.92156863, b: 0.6784314, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Park.mat.meta b/Samples~/MapboxComponents/Areas/Park.mat.meta new file mode 100644 index 000000000..c1be888c9 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Park.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96da09107c77b482fad0b37ae823518d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset new file mode 100644 index 000000000..1027d6524 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6183aeb8971b45778ba9d2c9fc425433, type: 3} + m_Name: ParksComponentVisualizerObject + m_EditorClassIdentifier: + Settings: + LayerName: landuse + GameObjectOffset: 0.0001 + Styles: + - ClassName: park + Material: {fileID: 2100000, guid: 96da09107c77b482fad0b37ae823518d, type: 2} + PushUp: 0.0001 + - ClassName: wood + Material: {fileID: 2100000, guid: b628e4b850b674e93b4c8166f827487d, type: 2} + PushUp: 0.0003 + - ClassName: grass + Material: {fileID: 2100000, guid: a7105d373b04b4d2089924fdba9f19b0, type: 2} + PushUp: 0.0002 + - ClassName: commercial_area + Material: {fileID: 2100000, guid: a64b34cd91bab49cf8f4d08438453d85, type: 2} + PushUp: 0.00005 + - ClassName: school + Material: {fileID: 2100000, guid: 7f47ac3ad8a8b47faa51ba920beb36ca, type: 2} + PushUp: 0.00035 + - ClassName: agriculture + Material: {fileID: 2100000, guid: a7105d373b04b4d2089924fdba9f19b0, type: 2} + PushUp: 0.00035 + - ClassName: pitch + Material: {fileID: 2100000, guid: 7b9c655e55da744e49145717d2dd09de, type: 2} + PushUp: 0.0004 + IsActive: 1 diff --git a/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta new file mode 100644 index 000000000..9b329591d --- /dev/null +++ b/Samples~/MapboxComponents/Areas/ParksComponentVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2654a2dbd021345d990964046e3e9300 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Pitch.mat b/Samples~/MapboxComponents/Areas/Pitch.mat new file mode 100644 index 000000000..775bcda75 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Pitch.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Pitch + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8156863, g: 0.95686275, b: 0.74509805, a: 1} + - _Color: {r: 0.8156863, g: 0.95686275, b: 0.74509805, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Pitch.mat.meta b/Samples~/MapboxComponents/Areas/Pitch.mat.meta new file mode 100644 index 000000000..05bca75c1 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Pitch.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b9c655e55da744e49145717d2dd09de +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/School.mat b/Samples~/MapboxComponents/Areas/School.mat new file mode 100644 index 000000000..64eeabbf5 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/School.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: School + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.9411765, g: 0.9019608, b: 0.81960785, a: 1} + - _Color: {r: 0.9411765, g: 0.9019608, b: 0.81960785, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/School.mat.meta b/Samples~/MapboxComponents/Areas/School.mat.meta new file mode 100644 index 000000000..d61505309 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/School.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7f47ac3ad8a8b47faa51ba920beb36ca +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Water.mat b/Samples~/MapboxComponents/Areas/Water.mat new file mode 100644 index 000000000..2d735a6e9 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Water.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Water + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.6, g: 0.8666667, b: 1, a: 1} + - _Color: {r: 0.6, g: 0.8666667, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Water.mat.meta b/Samples~/MapboxComponents/Areas/Water.mat.meta new file mode 100644 index 000000000..6892d3f9f --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Water.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6ebf8061831544cd7973cb57b1966259 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset new file mode 100644 index 000000000..9de60a5b3 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset @@ -0,0 +1,22 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6183aeb8971b45778ba9d2c9fc425433, type: 3} + m_Name: WaterComponentVisualizerObject.asset + m_EditorClassIdentifier: + Settings: + LayerName: water + GameObjectOffset: 0.0007 + Styles: + - ClassName: water + Material: {fileID: 2100000, guid: 6ebf8061831544cd7973cb57b1966259, type: 2} + PushUp: 0.0004 + IsActive: 1 diff --git a/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta new file mode 100644 index 000000000..f2346377a --- /dev/null +++ b/Samples~/MapboxComponents/Areas/WaterComponentVisualizerObject.asset.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 57618709630c44178bc0c0d905e015da +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Areas/Wood.mat b/Samples~/MapboxComponents/Areas/Wood.mat new file mode 100644 index 000000000..d92046e77 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Wood.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Wood + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.61960787, g: 0.88235295, b: 0.59607846, a: 1} + - _Color: {r: 0.61960775, g: 0.88235295, b: 0.5960784, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &8280136735912551859 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/Areas/Wood.mat.meta b/Samples~/MapboxComponents/Areas/Wood.mat.meta new file mode 100644 index 000000000..32a8796d7 --- /dev/null +++ b/Samples~/MapboxComponents/Areas/Wood.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b628e4b850b674e93b4c8166f827487d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Buildings.meta b/Samples~/MapboxComponents/Buildings.meta new file mode 100644 index 000000000..c9fe36b81 --- /dev/null +++ b/Samples~/MapboxComponents/Buildings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f0a1e617e1f224bf5ba9277daabff9f4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset new file mode 100644 index 000000000..7a986eb25 --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c6eb41c8ba1c475ab45d048dafafaf77, type: 3} + m_Name: BuildingComponentVisualizerObject + m_EditorClassIdentifier: + Settings: + EnableTerrainSnapping: 0 + RoundBuildingCorners: 1 + Material: {fileID: 2100000, guid: 16d95f6897a404ef3b6d50f7286fbfcf, type: 2} + ChamferExtrusionSettings: + OffsetInMeters: 1 + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + BasicExtrusionSettings: + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + IsActive: 1 diff --git a/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta new file mode 100644 index 000000000..4db308395 --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingComponentVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cc233d663d28c4daa88aed75870c52c5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat new file mode 100644 index 000000000..0f061c4bf --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat @@ -0,0 +1,155 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: BuildingMaterial + m_Shader: {fileID: -6465566751694194690, guid: 0873618690f77410691528b7ef785bcc, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 0, g: 0, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta new file mode 100644 index 000000000..628750e2c --- /dev/null +++ b/Samples~/MapboxComponents/Buildings/BuildingMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 16d95f6897a404ef3b6d50f7286fbfcf +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/GroundMaterial.mat b/Samples~/MapboxComponents/GroundMaterial.mat new file mode 100644 index 000000000..ad6eac0c5 --- /dev/null +++ b/Samples~/MapboxComponents/GroundMaterial.mat @@ -0,0 +1,133 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-8980196564406603438 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: GroundMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8962264, g: 0.8962264, b: 0.8962264, a: 1} + - _Color: {r: 0.8962264, g: 0.8962264, b: 0.8962264, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] diff --git a/Samples~/MapboxComponents/GroundMaterial.mat.meta b/Samples~/MapboxComponents/GroundMaterial.mat.meta new file mode 100644 index 000000000..d61ef0112 --- /dev/null +++ b/Samples~/MapboxComponents/GroundMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aaf04558f9b104d2ebc4e1c86464fc1c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Map.unity b/Samples~/MapboxComponents/Map.unity new file mode 100644 index 000000000..a322e0603 --- /dev/null +++ b/Samples~/MapboxComponents/Map.unity @@ -0,0 +1,829 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1001 &43046694 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1000012270391302, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_Name + value: Attribution + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 224000013044306136, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 4a24c3702bd1f46ac8d0579b0c199d4c, type: 3} +--- !u!1 &166616394 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 166616397} + - component: {fileID: 166616396} + - component: {fileID: 166616395} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &166616395 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 166616394} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &166616396 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 166616394} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &166616397 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 166616394} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &351425070 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 351425071} + - component: {fileID: 351425073} + - component: {fileID: 351425072} + m_Layer: 0 + m_Name: Sphere + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &351425071 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 351425070} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: -1, z: 0} + m_LocalScale: {x: 10, y: 10, z: 10} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 985205836} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!23 &351425072 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 351425070} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: ba676a16448114076b1bac2db9d5b88f, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &351425073 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 351425070} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &985205833 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 985205836} + - component: {fileID: 985205834} + - component: {fileID: 985205837} + m_Layer: 0 + m_Name: MapCamera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &985205834 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985205833} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.01 + far clip plane: 80 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &985205836 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985205833} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1.0000002, z: 1.0000002} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 351425071} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &985205837 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985205833} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.100000024 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1 &1316629397 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1316629400} + - component: {fileID: 1316629399} + - component: {fileID: 1316629407} + - component: {fileID: 1316629406} + m_Layer: 0 + m_Name: Map + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1316629399 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8987c930e6fb0479a907044faef4ab26, type: 3} + m_Name: + m_EditorClassIdentifier: + MapInformation: + _pitch: 45 + _bearing: 0 + _scale: 1000 + _latitudeLongitudeString: 52.502368,13.334595 + _zoom: 16 + UnityContext: + MapRoot: {fileID: 1316629400} + BaseTileRoot: {fileID: 1808235830} + RuntimeGenerationRoot: {fileID: 0} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: aaf04558f9b104d2ebc4e1c86464fc1c, type: 2} + TileProvider: {fileID: 0} + DataFetcher: {fileID: 0} + CacheManager: {fileID: 0} + InitializeOnStart: 1 +--- !u!4 &1316629400 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1808235830} + - {fileID: 2054936222} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1316629406 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e13965577acb456182a04c56586cb36e, type: 3} + m_Name: + m_EditorClassIdentifier: + _layerVisualizers: + - {fileID: 11400000, guid: cc233d663d28c4daa88aed75870c52c5, type: 2} + - {fileID: 11400000, guid: 7042be54feb564150ad5bd0a6a36dfd8, type: 2} + - {fileID: 11400000, guid: 57618709630c44178bc0c0d905e015da, type: 2} + - {fileID: 11400000, guid: 2654a2dbd021345d990964046e3e9300, type: 2} + - {fileID: 11400000, guid: ee7d380540f3448368c3eb1e8935378c, type: 2} +--- !u!114 &1316629407 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1316629397} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2fa13573d971f40649081b02ffc8366c, type: 3} + m_Name: + m_EditorClassIdentifier: + TileMaterials: + - {fileID: 2100000, guid: aaf04558f9b104d2ebc4e1c86464fc1c, type: 2} + InstanceMaterials: 0 + CacheSize: 25 +--- !u!1 &1498144748 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054936222} + - component: {fileID: 2054936221} + - component: {fileID: 2054936220} + m_Layer: 0 + m_Name: Utility + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1808235829 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1808235830} + m_Layer: 0 + m_Name: BaseTiles + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1808235830 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1808235829} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1316629400} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1909047952 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1909047955} + - component: {fileID: 1909047954} + - component: {fileID: 1909047953} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1909047953 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1909047952} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Version: 3 + m_UsePipelineSettings: 1 + m_AdditionalLightsShadowResolutionTier: 2 + m_LightLayerMask: 1 + m_RenderingLayers: 1 + m_CustomShadowLayers: 0 + m_ShadowLayerMask: 1 + m_ShadowRenderingLayers: 1 + m_LightCookieSize: {x: 1, y: 1} + m_LightCookieOffset: {x: 0, y: 0} + m_SoftShadowQuality: 0 +--- !u!108 &1909047954 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1909047952} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 0.41 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 0.402 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &1909047955 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1909047952} + serializedVersion: 2 + m_LocalRotation: {x: 0.1819419, y: 0.81802, z: -0.38144895, w: 0.39017567} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: 129, z: 0} +--- !u!114 &2054936220 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1498144748} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: caaa5942c50bf46e99b3904ca295889c, type: 3} + m_Name: + m_EditorClassIdentifier: + MapBehaviour: {fileID: 1316629399} + Camera: {fileID: 985205834} + ShiftRange: {x: 2000, y: 2000} +--- !u!114 &2054936221 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1498144748} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0439abe6d989b44a38eb2f8b633769fc, type: 3} + m_Name: + m_EditorClassIdentifier: + MapBehaviour: {fileID: 1316629399} + Camera: {fileID: 985205834} + Core: + Pitch: 30 + Bearing: 0 + Speed: -1 + CameraDistance: 20 + _targetPosition: {x: 0, y: 0, z: 0} + RotationSpeed: 50 + CameraCurve: {fileID: 11400000, guid: e887d93d4b690437fa30a6cc54adc7e8, type: 2} + CamDistanceMultiplier: 2 + ZoomSpeed: 2 +--- !u!4 &2054936222 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1498144748} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1316629400} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 985205836} + - {fileID: 1909047955} + - {fileID: 1316629400} + - {fileID: 166616397} + - {fileID: 43046694} diff --git a/Samples~/MapboxComponents/Map.unity.meta b/Samples~/MapboxComponents/Map.unity.meta new file mode 100644 index 000000000..9a6474342 --- /dev/null +++ b/Samples~/MapboxComponents/Map.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e80a52bde674545049e5b12142766d4f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/MapFog.mat b/Samples~/MapboxComponents/MapFog.mat new file mode 100644 index 000000000..d666caacf --- /dev/null +++ b/Samples~/MapboxComponents/MapFog.mat @@ -0,0 +1,137 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: MapFog + m_Shader: {fileID: -6465566751694194690, guid: 620ac0ed059f04d8eb20466a704ba8b4, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 1 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BlendOp: 0 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 0 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _FadeEnd: 0.15 + - _FadeStart: 0 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _SampleGI: 0 + - _Smoothness: 0.5 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8284087, g: 0.95864266, b: 0.9811321, a: 1} + - _Color: {r: 0.8284087, g: 0.95864266, b: 0.9811321, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] +--- !u!114 &1696498769756923639 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 diff --git a/Samples~/MapboxComponents/MapFog.mat.meta b/Samples~/MapboxComponents/MapFog.mat.meta new file mode 100644 index 000000000..ed8978e04 --- /dev/null +++ b/Samples~/MapboxComponents/MapFog.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ba676a16448114076b1bac2db9d5b88f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/MapboxFogShader.shadergraph b/Samples~/MapboxComponents/MapboxFogShader.shadergraph new file mode 100644 index 000000000..8a4197fca --- /dev/null +++ b/Samples~/MapboxComponents/MapboxFogShader.shadergraph @@ -0,0 +1,1110 @@ +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.GraphData", + "m_ObjectId": "aef4b9449d054df0bc58725679fb6101", + "m_Properties": [ + { + "m_Id": "e0b5dca034da4bde85c7b2ba1a1aad00" + }, + { + "m_Id": "e63ca906c8404258be11256ae2decf8c" + }, + { + "m_Id": "954b624585114582870bcc456e0b63ea" + } + ], + "m_Keywords": [], + "m_Dropdowns": [], + "m_CategoryData": [ + { + "m_Id": "132d1cc1314248f7928d2ee4c9c18b01" + } + ], + "m_Nodes": [ + { + "m_Id": "015aac73ba444a9a8e0d04aba79dae24" + }, + { + "m_Id": "16f5eef598cc4868ae55ee65ee1efe3d" + }, + { + "m_Id": "b60824ef93564f75b81efa94c1064b91" + }, + { + "m_Id": "e049db7645a8487d9a0013de4646c35f" + }, + { + "m_Id": "5bbbcb5587694ac38ed9e3804653e714" + }, + { + "m_Id": "4eeca085fcd64bda80568f8b7b04d71f" + }, + { + "m_Id": "72740d54c0b64fa7839a70287c81dcf7" + }, + { + "m_Id": "00d7e0c25d50412ba684c76491e01490" + }, + { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + { + "m_Id": "cb92c5825302468c9264328de973cbb1" + }, + { + "m_Id": "d0366037fe7543bf8402357f34ba43dd" + } + ], + "m_GroupDatas": [], + "m_StickyNoteDatas": [], + "m_Edges": [ + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "00d7e0c25d50412ba684c76491e01490" + }, + "m_SlotId": 2 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 2 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 3 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "4eeca085fcd64bda80568f8b7b04d71f" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "5bbbcb5587694ac38ed9e3804653e714" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "e049db7645a8487d9a0013de4646c35f" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "72740d54c0b64fa7839a70287c81dcf7" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "00d7e0c25d50412ba684c76491e01490" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "cb92c5825302468c9264328de973cbb1" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "d0366037fe7543bf8402357f34ba43dd" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "2b81c9123e6e4cd2b27f3428acc7e027" + }, + "m_SlotId": 1 + } + } + ], + "m_VertexContext": { + "m_Position": { + "x": 0.0, + "y": 0.0 + }, + "m_Blocks": [ + { + "m_Id": "015aac73ba444a9a8e0d04aba79dae24" + }, + { + "m_Id": "16f5eef598cc4868ae55ee65ee1efe3d" + }, + { + "m_Id": "b60824ef93564f75b81efa94c1064b91" + } + ] + }, + "m_FragmentContext": { + "m_Position": { + "x": 0.0, + "y": 200.0 + }, + "m_Blocks": [ + { + "m_Id": "e049db7645a8487d9a0013de4646c35f" + }, + { + "m_Id": "4eeca085fcd64bda80568f8b7b04d71f" + } + ] + }, + "m_PreviewData": { + "serializedMesh": { + "m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}", + "m_Guid": "" + }, + "preventRotation": false + }, + "m_Path": "Shader Graphs", + "m_GraphPrecision": 1, + "m_PreviewMode": 2, + "m_OutputNode": { + "m_Id": "" + }, + "m_SubDatas": [], + "m_ActiveTargets": [ + { + "m_Id": "593f6e3591614444b6eddeea2dd1cec6" + } + ] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.SplitNode", + "m_ObjectId": "00d7e0c25d50412ba684c76491e01490", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Split", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -623.5, + "y": 607.5, + "width": 119.0, + "height": 149.0 + } + }, + "m_Slots": [ + { + "m_Id": "157d215cbc5d4d138041822b690f658e" + }, + { + "m_Id": "39bd85397a834ca49db934596affbf3c" + }, + { + "m_Id": "7200592e6faa4d7897dc8227315ea91e" + }, + { + "m_Id": "b2f597e0fcfc45d2a3787fe60c57bd84" + }, + { + "m_Id": "3742cbb081964ed5ad625f275f7d3a41" + } + ], + "synonyms": [ + "separate" + ], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "015aac73ba444a9a8e0d04aba79dae24", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "db9ae5b54d97411f9aa4679c530da996" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Position" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.CategoryData", + "m_ObjectId": "132d1cc1314248f7928d2ee4c9c18b01", + "m_Name": "", + "m_ChildObjectList": [ + { + "m_Id": "e0b5dca034da4bde85c7b2ba1a1aad00" + }, + { + "m_Id": "e63ca906c8404258be11256ae2decf8c" + }, + { + "m_Id": "954b624585114582870bcc456e0b63ea" + } + ] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "157d215cbc5d4d138041822b690f658e", + "m_Id": 0, + "m_DisplayName": "In", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "In", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "16f5eef598cc4868ae55ee65ee1efe3d", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Normal", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "8d5a15b1265748539eeea023d710da2e" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Normal" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "1813a141aec6425fa4344b999e7b0354", + "m_Id": 1, + "m_DisplayName": "Edge2", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Edge2", + "m_StageCapability": 3, + "m_Value": { + "x": 1.0, + "y": 1.0, + "z": 1.0, + "w": 1.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "2b28eb0e8c7343ffb17abd3399780393", + "m_Id": 0, + "m_DisplayName": "Color", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.SmoothstepNode", + "m_ObjectId": "2b81c9123e6e4cd2b27f3428acc7e027", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Smoothstep", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -433.5, + "y": 489.5, + "width": 208.0, + "height": 326.0 + } + }, + "m_Slots": [ + { + "m_Id": "cb20a3e55c764573a5bf552fab79223d" + }, + { + "m_Id": "1813a141aec6425fa4344b999e7b0354" + }, + { + "m_Id": "5c72aec9be3a4444aa83786a24780d6b" + }, + { + "m_Id": "3367a07b6f8343d188be52099085bba0" + } + ], + "synonyms": [ + "curve" + ], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "3367a07b6f8343d188be52099085bba0", + "m_Id": 3, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "3742cbb081964ed5ad625f275f7d3a41", + "m_Id": 4, + "m_DisplayName": "A", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "A", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "39bd85397a834ca49db934596affbf3c", + "m_Id": 1, + "m_DisplayName": "R", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "R", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "4c74df6515614c61b327655a82ea8b3c", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "4eeca085fcd64bda80568f8b7b04d71f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Alpha", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "8897a82736784f30b1495e2c700f77d4" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Alpha" +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalTarget", + "m_ObjectId": "593f6e3591614444b6eddeea2dd1cec6", + "m_Datas": [], + "m_ActiveSubTarget": { + "m_Id": "8f69abcf5d13474a8dcfa5bd4ac0bd1d" + }, + "m_AllowMaterialOverride": false, + "m_SurfaceType": 1, + "m_ZTestMode": 4, + "m_ZWriteControl": 0, + "m_AlphaMode": 0, + "m_RenderFace": 1, + "m_AlphaClip": false, + "m_CastShadows": true, + "m_ReceiveShadows": true, + "m_SupportsLODCrossFade": false, + "m_CustomEditorGUI": "", + "m_SupportVFX": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "5bbbcb5587694ac38ed9e3804653e714", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -243.5, + "y": 238.0, + "width": 105.0, + "height": 34.0 + } + }, + "m_Slots": [ + { + "m_Id": "2b28eb0e8c7343ffb17abd3399780393" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e0b5dca034da4bde85c7b2ba1a1aad00" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "5c72aec9be3a4444aa83786a24780d6b", + "m_Id": 2, + "m_DisplayName": "In", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "In", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7200592e6faa4d7897dc8227315ea91e", + "m_Id": 2, + "m_DisplayName": "G", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "G", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.PositionNode", + "m_ObjectId": "72740d54c0b64fa7839a70287c81dcf7", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -844.5, + "y": 607.5, + "width": 206.0, + "height": 130.5 + } + }, + "m_Slots": [ + { + "m_Id": "4c74df6515614c61b327655a82ea8b3c" + } + ], + "synonyms": [ + "location" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 2, + "m_PositionSource": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "8458ec2b2d694369852b5de391563180", + "m_Id": 0, + "m_DisplayName": "FadeStart", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "8897a82736784f30b1495e2c700f77d4", + "m_Id": 0, + "m_DisplayName": "Alpha", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Alpha", + "m_StageCapability": 2, + "m_Value": 1.0, + "m_DefaultValue": 1.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot", + "m_ObjectId": "8d5a15b1265748539eeea023d710da2e", + "m_Id": 0, + "m_DisplayName": "Normal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Normal", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 2, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalUnlitSubTarget", + "m_ObjectId": "8f69abcf5d13474a8dcfa5bd4ac0bd1d" +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "954b624585114582870bcc456e0b63ea", + "m_Guid": { + "m_GuidSerialized": "3163ff8a-89c9-4619-a66c-23f24036acf1" + }, + "m_Name": "FadeEnd", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "FadeEnd", + "m_DefaultReferenceName": "_FadeEnd", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1.0, + "m_FloatType": 0, + "m_RangeValues": { + "x": 0.0, + "y": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "b2f597e0fcfc45d2a3787fe60c57bd84", + "m_Id": 3, + "m_DisplayName": "B", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "B", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "b60824ef93564f75b81efa94c1064b91", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Tangent", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "ef1df20f571f41b9a26921f5c18ae027" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Tangent" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "c499fb61a7964c82a88c20e02cb56253", + "m_Id": 0, + "m_DisplayName": "Base Color", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "BaseColor", + "m_StageCapability": 2, + "m_Value": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_ColorMode": 0, + "m_DefaultColor": { + "r": 0.5, + "g": 0.5, + "b": 0.5, + "a": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot", + "m_ObjectId": "cb20a3e55c764573a5bf552fab79223d", + "m_Id": 0, + "m_DisplayName": "Edge1", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Edge1", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "cb92c5825302468c9264328de973cbb1", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -611.0, + "y": 518.5, + "width": 126.5, + "height": 34.0 + } + }, + "m_Slots": [ + { + "m_Id": "8458ec2b2d694369852b5de391563180" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e63ca906c8404258be11256ae2decf8c" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "d0366037fe7543bf8402357f34ba43dd", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -611.0, + "y": 552.5, + "width": 121.0, + "height": 34.0 + } + }, + "m_Slots": [ + { + "m_Id": "e714d4bb90754bb0a674c4fd86009080" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "954b624585114582870bcc456e0b63ea" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PositionMaterialSlot", + "m_ObjectId": "db9ae5b54d97411f9aa4679c530da996", + "m_Id": 0, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "e049db7645a8487d9a0013de4646c35f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.BaseColor", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "c499fb61a7964c82a88c20e02cb56253" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.BaseColor" +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "e0b5dca034da4bde85c7b2ba1a1aad00", + "m_Guid": { + "m_GuidSerialized": "9eeb467d-44c4-4475-97ec-1ec908914044" + }, + "m_Name": "Color", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Color", + "m_DefaultReferenceName": "_Color", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 0.0 + }, + "isMainColor": false, + "m_ColorMode": 0 +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "e63ca906c8404258be11256ae2decf8c", + "m_Guid": { + "m_GuidSerialized": "73e4ec77-798f-45af-bd80-f33ad0fee628" + }, + "m_Name": "FadeStart", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "FadeStart", + "m_DefaultReferenceName": "_FadeStart", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.0, + "m_FloatType": 0, + "m_RangeValues": { + "x": 0.0, + "y": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e714d4bb90754bb0a674c4fd86009080", + "m_Id": 0, + "m_DisplayName": "FadeEnd", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.TangentMaterialSlot", + "m_ObjectId": "ef1df20f571f41b9a26921f5c18ae027", + "m_Id": 0, + "m_DisplayName": "Tangent", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Tangent", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + diff --git a/Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta b/Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta new file mode 100644 index 000000000..482fd1ae5 --- /dev/null +++ b/Samples~/MapboxComponents/MapboxFogShader.shadergraph.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 620ac0ed059f04d8eb20466a704ba8b4 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} diff --git a/Samples~/MapboxComponents/Road.meta b/Samples~/MapboxComponents/Road.meta new file mode 100644 index 000000000..d155dfec2 --- /dev/null +++ b/Samples~/MapboxComponents/Road.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: afe816260877949a3a3b19dd364024f6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/PathRoadMaterial.mat b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat new file mode 100644 index 000000000..7e5d20c71 --- /dev/null +++ b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat @@ -0,0 +1,160 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: PathRoadMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 2000 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 2800000, guid: c769830c8128948f0b5f101c50fe6043, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DecalMeshBiasType: 0 + - _DecalMeshDepthBias: 0 + - _DecalMeshViewBias: 0 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DrawOrder: 0 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.8396226, g: 0.8198202, b: 0.8198202, a: 1} + - _Color: {r: 0.8396226, g: 0.8198202, b: 0.8198202, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 1, g: 116.5, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta new file mode 100644 index 000000000..4b906db36 --- /dev/null +++ b/Samples~/MapboxComponents/Road/PathRoadMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3c69f315ce7c4100a0ff6f40305bfe9 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset new file mode 100644 index 000000000..af21efc58 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e926a31006c7a440ea45ffe7b2bcfff6, type: 3} + m_Name: RoadLayerVisualizerObject + m_EditorClassIdentifier: + Settings: + GameObjectOffset: 0.001 + RandomOffsetRange: 0.0002 + RoadStyleSheet: {fileID: 11400000, guid: 59e08c3bf126a4a85ad131ef30cd6176, type: 2} + IsActive: 1 diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta new file mode 100644 index 000000000..446270361 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7042be54feb564150ad5bd0a6a36dfd8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset new file mode 100644 index 000000000..3688fac61 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e926a31006c7a440ea45ffe7b2bcfff6, type: 3} + m_Name: RoadLayerVisualizerObjectOutline + m_EditorClassIdentifier: + Settings: + GameObjectOffset: 0.0008 + RandomOffsetRange: 0.0002 + RoadStyleSheet: {fileID: 11400000, guid: e733a7876e9c54e23ad4d5a984206d6c, type: 2} + IsActive: 1 diff --git a/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta new file mode 100644 index 000000000..051028f49 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadLayerVisualizerObjectOutline.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee7d380540f3448368c3eb1e8935378c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadMaterial.mat b/Samples~/MapboxComponents/Road/RoadMaterial.mat new file mode 100644 index 000000000..4c8a4620a --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadMaterial.mat @@ -0,0 +1,164 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 10 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RoadMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 2000 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 2800000, guid: 1db0b07d278894ec1b85003b5886e4a8, type: 3} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 2800000, guid: c769830c8128948f0b5f101c50fe6043, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DecalMeshBiasType: 0 + - _DecalMeshDepthBias: 0 + - _DecalMeshViewBias: 0 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DrawOrder: 0 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.509434, g: 0.509434, b: 0.509434, a: 1} + - _Color: {r: 0.509434, g: 0.509434, b: 0.509434, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 1, g: 116.5, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/RoadMaterial.mat.meta b/Samples~/MapboxComponents/Road/RoadMaterial.mat.meta new file mode 100644 index 000000000..a547942ab --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8917c0d338e58481388fbe080cf36caf +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat new file mode 100644 index 000000000..817772aa2 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat @@ -0,0 +1,156 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RoadOutlineMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 0.64140344, b: 0.3537736, a: 1} + - _Color: {r: 1, g: 0.64140344, b: 0.35377356, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 0, g: 0, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta new file mode 100644 index 000000000..9d3bc13bd --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadOutlineMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3a006dbd53abf4e029c02c32b8753395 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheet.asset b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset new file mode 100644 index 000000000..5981215fe --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset @@ -0,0 +1,158 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-5417554272265697845 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_1_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-1538625035922140899 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-245792505840917342 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_Name: RoadStyleSheet + m_EditorClassIdentifier: + Styles: + - Name: secondary,tertiary + Filters: + Filters: + - rid: 5722777937161748527 + Type: 0 + Width: 4.8 + PushUp: 0.0008 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: primary + Filters: + Filters: + - rid: 5722777937161748529 + Type: 0 + Width: 5.2 + PushUp: 0.001 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: street,street_limited,motorway_link + Filters: + Filters: + - rid: 5722777937161748530 + Type: 0 + Width: 3.6 + PushUp: 0.0004 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: service + Filters: + Filters: + - rid: 5722777937161748531 + Type: 0 + Width: 1.8 + PushUp: 0.0002 + Material: {fileID: 2100000, guid: d449f6eb93a644716baa6f1f4c730813, type: 2} + - Name: path + Filters: + Filters: + - rid: 5722777987101753359 + - rid: 5722777987101753361 + - rid: 5722777987101753362 + Type: 0 + Width: 1 + PushUp: -0.0005 + Material: {fileID: 2100000, guid: c3c69f315ce7c4100a0ff6f40305bfe9, type: 2} + references: + version: 2 + RefIds: + - rid: 5722777937161748527 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: secondary,tertiary + Invert: 0 + - rid: 5722777937161748529 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: primary + Invert: 0 + - rid: 5722777937161748530 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: street,street_limited,motorway_link + Invert: 0 + - rid: 5722777937161748531 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: service + Invert: 0 + - rid: 5722777987101753359 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: path + Invert: 0 + - rid: 5722777987101753361 + type: {class: RoadStructureFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: tunnel + Invert: 1 + - rid: 5722777987101753362 + type: {class: RoadTypeFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: steps + Invert: 1 +--- !u!114 &6606382917155413568 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta new file mode 100644 index 000000000..33cb5868a --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59e08c3bf126a4a85ad131ef30cd6176 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset new file mode 100644 index 000000000..6f8d698be --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset @@ -0,0 +1,116 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-5417554272265697845 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_1_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-1538625035922140899 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &-245792505840917342 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_Name: RoadStyleSheetOutline + m_EditorClassIdentifier: + Styles: + - Name: secondary,tertiary + Filters: + Filters: + - rid: 5722777937161748527 + Type: 0 + Width: 7.8 + PushUp: 0.0001 + Material: {fileID: 2100000, guid: 3a006dbd53abf4e029c02c32b8753395, type: 2} + - Name: primary + Filters: + Filters: + - rid: 5722777937161748529 + Type: 0 + Width: 8.2 + PushUp: 0.0002 + Material: {fileID: 2100000, guid: 3a006dbd53abf4e029c02c32b8753395, type: 2} + - Name: street,street_limited + Filters: + Filters: + - rid: 5722777937161748530 + Type: 0 + Width: 6.6 + PushUp: 0.0001 + Material: {fileID: 2100000, guid: 3a006dbd53abf4e029c02c32b8753395, type: 2} + references: + version: 2 + RefIds: + - rid: 5722777937161748527 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: secondary,tertiary + Invert: 0 + - rid: 5722777937161748529 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: primary + Invert: 0 + - rid: 5722777937161748530 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: street,street_limited + Invert: 0 +--- !u!114 &6606382917155413568 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: RoadStyle_0_Filters + m_EditorClassIdentifier: + Filters: [] + Type: 0 diff --git a/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta new file mode 100644 index 000000000..3db576ac3 --- /dev/null +++ b/Samples~/MapboxComponents/Road/RoadStyleSheetOutline.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e733a7876e9c54e23ad4d5a984206d6c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat new file mode 100644 index 000000000..82546b10d --- /dev/null +++ b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat @@ -0,0 +1,160 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-6790937051986667402 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 7 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ServiceRoadMaterial + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 2000 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 50} + m_Offset: {x: 0, y: 0} + - _MainTexture: + m_Texture: {fileID: 2800000, guid: c769830c8128948f0b5f101c50fe6043, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _BUILTIN_QueueControl: 0 + - _BUILTIN_QueueOffset: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DecalMeshBiasType: 0 + - _DecalMeshDepthBias: 0 + - _DecalMeshViewBias: 0 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DrawOrder: 0 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueControl: 0 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0.52156866, g: 0.5254902, b: 0.52156866, a: 1} + - _Color: {r: 0.5215686, g: 0.5254902, b: 0.5215686, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _Offset: {r: 0, g: 0, b: 0, a: 0} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + - _Tiling: {r: 1, g: 116.5, b: 0, a: 0} + - _visibleRange: {r: 0, g: 0, b: 0, a: 0} + m_BuildTextureStacks: [] +--- !u!114 &5351528839697624954 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 639247ca83abc874e893eb93af2b5e44, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 0 diff --git a/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta new file mode 100644 index 000000000..b02087840 --- /dev/null +++ b/Samples~/MapboxComponents/Road/ServiceRoadMaterial.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d449f6eb93a644716baa6f1f4c730813 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/MapboxComponents/Road/road.preset b/Samples~/MapboxComponents/Road/road.preset new file mode 100644 index 000000000..927610640 --- /dev/null +++ b/Samples~/MapboxComponents/Road/road.preset @@ -0,0 +1,291 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: road + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.size + value: 5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Name + value: secondary,tertiary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Filters.Filters.Array.data[0] + value: 5722777937161748527 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Width + value: 4.8 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].PushUp + value: 0.0008 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[0].Material + value: + objectReference: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Name + value: primary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Filters.Filters.Array.data[0] + value: 5722777937161748529 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Width + value: 5.2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].PushUp + value: 0.001 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[1].Material + value: + objectReference: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Name + value: street,street_limited + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Filters.Filters.Array.data[0] + value: 5722777937161748530 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Width + value: 3.6 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].PushUp + value: 0.0004 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[2].Material + value: + objectReference: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Name + value: service + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Filters.Filters.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Filters.Filters.Array.data[0] + value: 5722777937161748531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Width + value: 1.8 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].PushUp + value: 0.0002 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[3].Material + value: + objectReference: {fileID: 2100000, guid: d449f6eb93a644716baa6f1f4c730813, type: 2} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Name + value: path + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.size + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.data[0] + value: 5722777987101753359 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.data[1] + value: 5722777987101753361 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Filters.Array.data[2] + value: 5722777987101753362 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Filters.Type + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Width + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].PushUp + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: Styles.Array.data[4].Material + value: + objectReference: {fileID: 2100000, guid: c3c69f315ce7c4100a0ff6f40305bfe9, type: 2} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527].CheckOperation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527].FilterString + value: secondary,tertiary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748527].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529].FilterString + value: primary + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748529].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530].CheckOperation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530].FilterString + value: street,street_limited + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748530].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531].FilterString + value: service + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777937161748531].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadClassFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359].FilterString + value: path + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753359].Invert + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadStructureFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361].FilterString + value: tunnel + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753361].Invert + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362] + value: MapboxComponentSystem Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer.RoadTypeFilter + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362].CheckOperation + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362].FilterString + value: steps + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: managedReferences[5722777987101753362].Invert + value: 1 + objectReference: {fileID: 0} + m_ExcludedProperties: [] diff --git a/Samples~/MapboxComponents/Road/road.preset.meta b/Samples~/MapboxComponents/Road/road.preset.meta new file mode 100644 index 000000000..6d9f71b20 --- /dev/null +++ b/Samples~/MapboxComponents/Road/road.preset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38743b7a21f944113b43bee79ecf030a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/WorldMapViewer/Map.unity b/Samples~/WorldMapViewer/Map.unity index 453848657..9d8d116b2 100644 --- a/Samples~/WorldMapViewer/Map.unity +++ b/Samples~/WorldMapViewer/Map.unity @@ -332,7 +332,6 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Settings: - ElevationLayerType: 1 LoadBackgroundTextures: 1 UseShaderTerrain: 1 DataSettings: @@ -340,7 +339,7 @@ MonoBehaviour: UseNonReadableTextures: 1 CacheSize: 100 ClampDataLevelToMax: 14 - RejectTilesOutsideZoom: {x: 10, y: 25} + RejectTilesOutsideZoom: {x: 2, y: 25} ElevationLayerProperties: modificationOptions: SimplificationFactor: 1 @@ -672,6 +671,7 @@ MonoBehaviour: BaseTileRoot: {fileID: 1808235830} RuntimeGenerationRoot: {fileID: 0} _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 2054936219} DataFetcher: {fileID: 0} CacheManager: {fileID: 0} diff --git a/Tests/Editor/BaseModule/ConversionTests.cs b/Tests/Editor/BaseModule/ConversionTests.cs index da7e3830a..89f365a3b 100644 --- a/Tests/Editor/BaseModule/ConversionTests.cs +++ b/Tests/Editor/BaseModule/ConversionTests.cs @@ -1,4 +1,5 @@ using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; using Mapbox.BaseModule.Utilities; using NUnit.Framework; using UnityEngine; @@ -39,6 +40,107 @@ public void StringToLatLngToMercatorToLatLng() Assert.AreEqual(latlng.Longitude, newLatlng.Longitude, 0.001d); } + [Test] + public void WebMercator_RoundTrip_PreservesCoordinatesAcrossWorld() + { + // Sample diverse latitudes/longitudes including hemispheres, equator, + // and near-the-pole values. Mercator only spans ±85.0511° so high + // latitudes are intentionally near that limit, not beyond. + var samples = new[] + { + new LatitudeLongitude(0, 0), // Null Island + new LatitudeLongitude(40.7128, -74.0060), // New York + new LatitudeLongitude(51.5074, -0.1278), // London + new LatitudeLongitude(-33.8688, 151.2093), // Sydney + new LatitudeLongitude(35.6895, 139.6917), // Tokyo + new LatitudeLongitude(-22.9068, -43.1729), // Rio de Janeiro + new LatitudeLongitude(85, 179.99), // near north pole + antimeridian + new LatitudeLongitude(-85, -179.99), // near south pole + antimeridian + }; + + foreach (var ll in samples) + { + var mercator = Conversions.LatitudeLongitudeToWebMercator(ll); + var back = Conversions.WebMercatorToLatLon(mercator); + + // 1e-6 degrees ≈ 11cm at the equator. The transform is analytic; this + // catches sign-flip bugs and the half-circumference offset, not float drift. + Assert.AreEqual(ll.Latitude, back.Latitude, 1e-6, $"Latitude round-trip failed for {ll}"); + Assert.AreEqual(ll.Longitude, back.Longitude, 1e-6, $"Longitude round-trip failed for {ll}"); + } + } + + [Test] + public void LatLngToTileId_RoundTripsThroughTileCenter() + { + // Each lat/lng → tile-id → tile-center-lat/lng → tile-id should land + // back on the same tile. Across multiple zoom levels. + var samples = new[] + { + new LatitudeLongitude(40.7128, -74.0060), // New York + new LatitudeLongitude(48.8566, 2.3522), // Paris + new LatitudeLongitude(-22.9068, -43.1729), // Rio + }; + + for (int z = 2; z <= 18; z += 4) + { + foreach (var ll in samples) + { + var tileId = Conversions.LatitudeLongitudeToTileId(ll, z); + var bounds = Conversions.TileIdToBounds(tileId.Canonical); + var tileCenter = bounds.Center; + var roundtrip = Conversions.LatitudeLongitudeToTileId(tileCenter, z); + + Assert.AreEqual(tileId.ToString(), roundtrip.ToString(), + $"Tile round-trip failed at z={z} for {ll}"); + } + } + } + + [Test] + public void Tile01_CenterMapsToTileCenter() + { + // (0.5, 0.5) maps to the tile's center regardless of any convention + // about which corner (0,0) or (1,1) is. Useful invariant; corner + // mappings are tested separately if needed. + var tileId = new CanonicalTileId(14, 9647, 12321); + var bounds = Conversions.TileIdToBounds(tileId); + + var center = Conversions.Tile01ToLatitudeLongitude(new Vector2(0.5f, 0.5f), tileId); + + Assert.AreEqual(bounds.Center.Latitude, center.Latitude, 1e-6); + Assert.AreEqual(bounds.Center.Longitude, center.Longitude, 1e-6); + } + + [Test] + public void TileSizeInUnitySpace_HalvesEveryZoomLevel() + { + // Mercator tile pyramid: at each zoom level, tile size halves. + const float scale = 1f; + float previousSize = -1f; + for (int z = 0; z <= 20; z++) + { + var size = Conversions.TileSizeInUnitySpace(z, scale); + Assert.Greater(size, 0f, $"Tile size at z={z} should be positive."); + if (previousSize > 0) + { + // Allow tiny float drift but the ratio should be ~2. + Assert.AreEqual(previousSize / 2f, size, previousSize * 1e-5f, + $"Tile size at z={z} should be half of z={z - 1}."); + } + previousSize = size; + } + } + + [Test] + public void TileSizeInUnitySpace_ScalesLinearlyWithScale() + { + // Doubling the map scale halves the tile size in Unity units. + var sizeAtScale1 = Conversions.TileSizeInUnitySpace(10, 1f); + var sizeAtScale2 = Conversions.TileSizeInUnitySpace(10, 2f); + Assert.AreEqual(sizeAtScale1 / 2f, sizeAtScale2, 1e-4f); + } + [Test] public void Test_TileEdgeSizeInMercator() { diff --git a/Tests/Editor/BaseModule/DataCompressionTests.cs b/Tests/Editor/BaseModule/DataCompressionTests.cs index 2dfa94c6a..004164f34 100644 --- a/Tests/Editor/BaseModule/DataCompressionTests.cs +++ b/Tests/Editor/BaseModule/DataCompressionTests.cs @@ -13,10 +13,11 @@ public class DataCompressionTests { private ResilientWebRequestFileSource _fs; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } @@ -75,7 +76,11 @@ public IEnumerator Decompress() Assert.AreEqual(buffer.Length, Compression.Decompress(buffer).Length); // EditMode on OSX #elif UNITY_EDITOR && (UNITY_IOS || UNITY_ANDROID) // PlayMode tests in Editor Debug.Log("EditMode tests in Editor"); - Assert.Less(buffer.Length, Compression.Decompress(buffer).Length); + // LessOrEqual (not Less) so the test passes whether the response arrives + // gzipped (decompression makes it strictly larger) or already + // transparently decompressed by UnityWebRequest / the server (in which + // case decompression is a no-op and the sizes match). + Assert.LessOrEqual(buffer.Length, Compression.Decompress(buffer).Length); #elif !UNITY_EDITOR && (UNITY_EDITOR_OSX || UNITY_IOS || UNITY_ANDROID) // PlayMode tests on device Debug.Log("PlayMode tests on device"); Assert.AreEqual(buffer.Length, Compression.Decompress(buffer).Length); diff --git a/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs new file mode 100644 index 000000000..b49756a8e --- /dev/null +++ b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs @@ -0,0 +1,94 @@ +using Mapbox.BaseModule.Data.DataFetchers; +using NUnit.Framework; + +namespace Mapbox.BaseModuleTests +{ + /// + /// Tests use unique sizes per test method to avoid cross-test contamination from + /// the pool's static state. + /// + public class ElevationArrayPoolTests + { + [Test] + public void Rent_AllocatesNewArrayOfRequestedSize_WhenPoolIsEmpty() + { + var array = ElevationArrayPool.Rent(1001); + + Assert.IsNotNull(array); + Assert.AreEqual(1001, array.Length); + } + + [Test] + public void ReturnThenRent_ReusesSameBuffer() + { + var first = ElevationArrayPool.Rent(1002); + ElevationArrayPool.Return(first); + + var second = ElevationArrayPool.Rent(1002); + + Assert.AreSame(first, second, "Pool should hand back the same buffer it just received."); + } + + [Test] + public void Rent_DifferentSizes_ReturnsDistinctBuffers() + { + var a = ElevationArrayPool.Rent(1003); + var b = ElevationArrayPool.Rent(1004); + + Assert.AreNotSame(a, b); + Assert.AreEqual(1003, a.Length); + Assert.AreEqual(1004, b.Length); + } + + [Test] + public void Return_NullArray_IsNoOp() + { + // Should not throw. + Assert.DoesNotThrow(() => ElevationArrayPool.Return(null)); + } + + [Test] + public void Pool_CapsAtMaxPerSizeDepth_DroppingExcessToGC() + { + // MaxPerSizeDepth is 32 (private const). Push 40 distinct buffers of the + // same size and confirm at most 32 distinct buffers (by reference identity) + // can ever be rented back. The remaining 8 must have been dropped to GC. + const int size = 1005; + const int pushCount = 40; + + var pushedSet = new System.Collections.Generic.HashSet(ReferenceEqualityComparer.Instance); + for (int i = 0; i < pushCount; i++) + { + var arr = new float[size]; + pushedSet.Add(arr); + ElevationArrayPool.Return(arr); + } + + // Drain the pool: keep renting until we get something that isn't in the + // original pushed set (i.e. a freshly allocated array, meaning the pool + // was empty for this size). Count distinct pushed buffers seen. + var seen = new System.Collections.Generic.HashSet(ReferenceEqualityComparer.Instance); + while (true) + { + var rented = ElevationArrayPool.Rent(size); + if (!pushedSet.Contains(rented)) + { + break; // freshly allocated → pool is drained + } + seen.Add(rented); + } + + Assert.LessOrEqual(seen.Count, 32, "Pool retained more buffers than its documented cap."); + Assert.Greater(seen.Count, 0, "Pool should have retained at least one buffer for reuse."); + } + + // Reference-equality comparer for HashSet — default equality + // compares contents element-wise, which isn't what we want. + private sealed class ReferenceEqualityComparer : System.Collections.Generic.IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + public bool Equals(float[] x, float[] y) => ReferenceEquals(x, y); + public int GetHashCode(float[] obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta new file mode 100644 index 000000000..a1eefac79 --- /dev/null +++ b/Tests/Editor/BaseModule/ElevationArrayPoolTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 614c19621ee044cdd9165bd1a874e055 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/BaseModule/FileSourceTests.cs b/Tests/Editor/BaseModule/FileSourceTests.cs index 6cf7e7e0f..ae3c085e3 100644 --- a/Tests/Editor/BaseModule/FileSourceTests.cs +++ b/Tests/Editor/BaseModule/FileSourceTests.cs @@ -11,10 +11,11 @@ public class FileSourceTests { private ResilientWebRequestFileSource _fs; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/MapInformationTests.cs b/Tests/Editor/BaseModule/MapInformationTests.cs new file mode 100644 index 000000000..29187ad86 --- /dev/null +++ b/Tests/Editor/BaseModule/MapInformationTests.cs @@ -0,0 +1,55 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Map; +using NUnit.Framework; + +namespace Mapbox.BaseModuleTests +{ + public class MapInformationTests + { + [Test] + public void Initialize_ResetsTerrainBoundsToDefaults() + { + // Simulate a fresh instance with pre-mutated terrain bounds — proves the + // Initialize() reset is doing its job. Today's field initializers cover + // the common case, but this guards against future paths that reuse an + // existing MapInformation instance. + var info = new MapInformation("0,0"); + info.Terrain.MinElevation = -999f; + info.Terrain.MaxElevation = 9999f; + + info.Initialize(new LatitudeLongitude(0, 0)); + + Assert.AreEqual(TerrainInfo.DefaultMinElevation, info.Terrain.MinElevation); + Assert.AreEqual(TerrainInfo.DefaultMaxElevation, info.Terrain.MaxElevation); + } + + [Test] + public void Initialize_SetsLatitudeLongitude() + { + var info = new MapInformation("0,0"); + var target = new LatitudeLongitude(40.7484, -73.9857); + + info.Initialize(target); + + Assert.AreEqual(target.Latitude, info.LatitudeLongitude.Latitude); + Assert.AreEqual(target.Longitude, info.LatitudeLongitude.Longitude); + } + + [Test] + public void Initialize_IsIdempotent() + { + var info = new MapInformation("0,0"); + info.Initialize(new LatitudeLongitude(10, 20)); + + // Mutate terrain after first init, then call Initialize again — the + // guard at the top should short-circuit and leave our mutation in place. + info.Terrain.MinElevation = -500f; + info.Initialize(new LatitudeLongitude(50, 50)); + + Assert.AreEqual(-500f, info.Terrain.MinElevation, + "Second Initialize call should be a no-op and must not reset terrain bounds."); + Assert.AreEqual(10, info.LatitudeLongitude.Latitude, + "Second Initialize call should not overwrite the lat/lon from the first call."); + } + } +} diff --git a/Tests/Editor/BaseModule/MapInformationTests.cs.meta b/Tests/Editor/BaseModule/MapInformationTests.cs.meta new file mode 100644 index 000000000..a8f0a47a0 --- /dev/null +++ b/Tests/Editor/BaseModule/MapInformationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b37d971e7300b47e99af22ed149f50f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/BaseModule/TerrainInfoTests.cs b/Tests/Editor/BaseModule/TerrainInfoTests.cs new file mode 100644 index 000000000..8190be6a4 --- /dev/null +++ b/Tests/Editor/BaseModule/TerrainInfoTests.cs @@ -0,0 +1,26 @@ +using Mapbox.BaseModule.Map; +using NUnit.Framework; + +namespace Mapbox.BaseModuleTests +{ + public class TerrainInfoTests + { + [Test] + public void DefaultConstants_HaveExpectedValues() + { + // Documented contract: Min=0, Max=5000. Conservative bounds that admit any + // plausible mountain range so steep terrain isn't frustum-culled on first paint. + Assert.AreEqual(0f, TerrainInfo.DefaultMinElevation); + Assert.AreEqual(5000f, TerrainInfo.DefaultMaxElevation); + } + + [Test] + public void NewInstance_FieldsInitializedToDefaults() + { + var info = new TerrainInfo(); + + Assert.AreEqual(TerrainInfo.DefaultMinElevation, info.MinElevation); + Assert.AreEqual(TerrainInfo.DefaultMaxElevation, info.MaxElevation); + } + } +} diff --git a/Tests/Editor/BaseModule/TerrainInfoTests.cs.meta b/Tests/Editor/BaseModule/TerrainInfoTests.cs.meta new file mode 100644 index 000000000..c1a8ca616 --- /dev/null +++ b/Tests/Editor/BaseModule/TerrainInfoTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb886f22bc82a4306b67ad743458bfc2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/BaseModule/TerrainSourceTests.cs b/Tests/Editor/BaseModule/TerrainSourceTests.cs index a256ef765..aad66df90 100644 --- a/Tests/Editor/BaseModule/TerrainSourceTests.cs +++ b/Tests/Editor/BaseModule/TerrainSourceTests.cs @@ -28,10 +28,14 @@ internal class TerrainSourceTests private CanonicalTileId _testTileId = new CanonicalTileId(16, 37310, 18968); private Texture2D _testTexture; private RasterData _testRasterData; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetUp() + [UnitySetUp] + public IEnumerator OneTimeSetUp() { + if (_initialized) yield break; + _initialized = true; + _testTexture = Texture2D.whiteTexture; _testRasterData = new TerrainData() { @@ -44,6 +48,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Editor/BaseModule/TileJSONTest.cs b/Tests/Editor/BaseModule/TileJSONTest.cs index 597cd7961..f56dc66d8 100644 --- a/Tests/Editor/BaseModule/TileJSONTest.cs +++ b/Tests/Editor/BaseModule/TileJSONTest.cs @@ -11,10 +11,11 @@ internal class TileJSONTest { private DataFetchingManager _dataFetcher; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _dataFetcher = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); } diff --git a/Tests/Editor/BaseModule/TokenTest.cs b/Tests/Editor/BaseModule/TokenTest.cs index 927a7b4ae..bb45a25cb 100644 --- a/Tests/Editor/BaseModule/TokenTest.cs +++ b/Tests/Editor/BaseModule/TokenTest.cs @@ -13,10 +13,11 @@ public class TokenTest private string _configAccessToken; private Func _configSkuToken; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _tokenApi = new MapboxTokenApi(); _configAccessToken = mapboxContext.GetAccessToken(); _configSkuToken = mapboxContext.GetSkuToken; diff --git a/Tests/Editor/DirectionsApi.meta b/Tests/Editor/DirectionsApi.meta new file mode 100644 index 000000000..cda42a46a --- /dev/null +++ b/Tests/Editor/DirectionsApi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6146f86a73872419d960eeed7ee273d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/BearingFilterTests.cs b/Tests/Editor/DirectionsApi/BearingFilterTests.cs new file mode 100644 index 000000000..c22578bf0 --- /dev/null +++ b/Tests/Editor/DirectionsApi/BearingFilterTests.cs @@ -0,0 +1,145 @@ +using System; +using Mapbox.Utils; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class BearingFilterTests + { + [Test] + public void Constructor_SetsValues() + { + var filter = new BearingFilter(45, 10); + + Assert.AreEqual(45, filter.Bearing); + Assert.AreEqual(10, filter.Range); + } + + [Test] + public void Constructor_AcceptsNullValues() + { + var filter = new BearingFilter(null, null); + + Assert.IsNull(filter.Bearing); + Assert.IsNull(filter.Range); + } + + [Test] + public void Constructor_ThrowsException_WhenBearingIsNegative() + { + Assert.Throws(() => new BearingFilter(-1, 10)); + } + + [Test] + public void Constructor_ThrowsException_WhenBearingIsGreaterThan360() + { + Assert.Throws(() => new BearingFilter(361, 10)); + } + + [Test] + public void Constructor_AcceptsBearing_AtZero() + { + var filter = new BearingFilter(0, 10); + Assert.AreEqual(0, filter.Bearing); + } + + [Test] + public void Constructor_AcceptsBearing_At360() + { + var filter = new BearingFilter(360, 10); + Assert.AreEqual(360, filter.Bearing); + } + + [Test] + public void Constructor_ThrowsException_WhenRangeIsNegative() + { + Assert.Throws(() => new BearingFilter(45, -1)); + } + + [Test] + public void Constructor_ThrowsException_WhenRangeIsGreaterThan180() + { + Assert.Throws(() => new BearingFilter(45, 181)); + } + + [Test] + public void Constructor_AcceptsRange_AtZero() + { + var filter = new BearingFilter(45, 0); + Assert.AreEqual(0, filter.Range); + } + + [Test] + public void Constructor_AcceptsRange_At180() + { + var filter = new BearingFilter(45, 180); + Assert.AreEqual(180, filter.Range); + } + + [Test] + public void ToString_ReturnsCorrectFormat() + { + var filter = new BearingFilter(45.5, 10.5); + var result = filter.ToString(); + + Assert.AreEqual("45.5,10.5", result); + } + + [Test] + public void ToString_ReturnsEmptyString_WhenBearingIsNull() + { + var filter = new BearingFilter(null, 10); + var result = filter.ToString(); + + Assert.AreEqual(string.Empty, result); + } + + [Test] + public void ToString_ReturnsEmptyString_WhenRangeIsNull() + { + var filter = new BearingFilter(45, null); + var result = filter.ToString(); + + Assert.AreEqual(string.Empty, result); + } + + [Test] + public void ToString_ReturnsEmptyString_WhenBothAreNull() + { + var filter = new BearingFilter(null, null); + var result = filter.ToString(); + + Assert.AreEqual(string.Empty, result); + } + + [TestCase(0, 0, "0,0")] + [TestCase(90, 45, "90,45")] + [TestCase(180, 90, "180,90")] + [TestCase(270, 135, "270,135")] + [TestCase(360, 180, "360,180")] + public void ToString_ReturnsCorrectFormat_ForVariousAngles(double bearing, double range, string expected) + { + var filter = new BearingFilter(bearing, range); + var result = filter.ToString(); + + Assert.AreEqual(expected, result); + } + + [Test] + public void Constructor_AcceptsDecimalValues() + { + var filter = new BearingFilter(123.456, 78.9); + + Assert.AreEqual(123.456, filter.Bearing); + Assert.AreEqual(78.9, filter.Range); + } + + [Test] + public void Constructor_ThrowsException_OnlyWhenBearingIsInvalid() + { + // Valid bearing, invalid range should throw + Assert.Throws(() => new BearingFilter(45, 200)); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta b/Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta new file mode 100644 index 000000000..db8e03240 --- /dev/null +++ b/Tests/Editor/DirectionsApi/BearingFilterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 547449c5fbdd04746810e0c308b43db5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/DirectionResourceTests.cs b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs new file mode 100644 index 000000000..50195949e --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs @@ -0,0 +1,353 @@ +using System; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.DirectionsApi; +using Mapbox.Utils; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class DirectionResourceTests + { + private LatitudeLongitude[] _validCoordinates; + + [SetUp] + public void Setup() + { + _validCoordinates = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), // San Francisco + new LatitudeLongitude(34.0522, -118.2437) // Los Angeles + }; + } + + [Test] + public void Constructor_SetsCoordinatesAndProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + + Assert.AreEqual(2, resource.Coordinates.Length); + Assert.AreEqual(RoutingProfile.Driving, resource.RoutingProfile); + } + + [Test] + public void Coordinates_ThrowsException_WhenLessThan2() + { + var invalidCoords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194) + }; + + Assert.Throws(() => new DirectionResource(invalidCoords, RoutingProfile.Driving)); + } + + [Test] + public void Coordinates_ThrowsException_WhenMoreThan25() + { + var invalidCoords = new LatitudeLongitude[26]; + for (int i = 0; i < 26; i++) + { + invalidCoords[i] = new LatitudeLongitude(37.0 + i * 0.1, -122.0 + i * 0.1); + } + + Assert.Throws(() => new DirectionResource(invalidCoords, RoutingProfile.Driving)); + } + + [Test] + public void Coordinates_AcceptsExactly2Elements() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Walking); + Assert.AreEqual(2, resource.Coordinates.Length); + } + + [Test] + public void Coordinates_AcceptsExactly25Elements() + { + var coords = new LatitudeLongitude[25]; + for (int i = 0; i < 25; i++) + { + coords[i] = new LatitudeLongitude(37.0 + i * 0.1, -122.0 + i * 0.1); + } + + var resource = new DirectionResource(coords, RoutingProfile.Cycling); + Assert.AreEqual(25, resource.Coordinates.Length); + } + + [Test] + public void RoutingProfile_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.RoutingProfile = RoutingProfile.Walking; + + Assert.AreEqual(RoutingProfile.Walking, resource.RoutingProfile); + } + + [Test] + public void Alternatives_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Alternatives); + } + + [Test] + public void Alternatives_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Alternatives = true; + + Assert.IsTrue(resource.Alternatives.Value); + } + + [Test] + public void Steps_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Steps); + } + + [Test] + public void Steps_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Steps = true; + + Assert.IsTrue(resource.Steps.Value); + } + + [Test] + public void ContinueStraight_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.ContinueStraight); + } + + [Test] + public void ContinueStraight_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.ContinueStraight = false; + + Assert.IsFalse(resource.ContinueStraight.Value); + } + + [Test] + public void Overview_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Overview); + } + + [Test] + public void Overview_CanBeSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Overview = Overview.Full; + + Assert.AreEqual(Overview.Full, resource.Overview); + } + + [Test] + public void Bearings_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Bearings); + } + + [Test] + public void Bearings_ThrowsException_WhenCountMismatchesCoordinates() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var invalidBearings = new BearingFilter[] + { + new BearingFilter(45, 10) + }; + + Assert.Throws(() => resource.Bearings = invalidBearings); + } + + [Test] + public void Bearings_AcceptsCorrectCount() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var validBearings = new BearingFilter[] + { + new BearingFilter(45, 10), + new BearingFilter(90, 20) + }; + + resource.Bearings = validBearings; + Assert.AreEqual(2, resource.Bearings.Length); + } + + [Test] + public void Radiuses_DefaultsToNull() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + Assert.IsNull(resource.Radiuses); + } + + [Test] + public void Radiuses_ThrowsException_WhenCountMismatchesCoordinates() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var invalidRadiuses = new double[] { 100.0 }; + + Assert.Throws(() => resource.Radiuses = invalidRadiuses); + } + + [Test] + public void Radiuses_ThrowsException_WhenValueIsZeroOrNegative() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var invalidRadiuses = new double[] { 100.0, 0 }; + + Assert.Throws(() => resource.Radiuses = invalidRadiuses); + } + + [Test] + public void Radiuses_AcceptsPositiveValues() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var validRadiuses = new double[] { 100.0, 200.0 }; + + resource.Radiuses = validRadiuses; + Assert.AreEqual(2, resource.Radiuses.Length); + Assert.AreEqual(100.0, resource.Radiuses[0]); + Assert.AreEqual(200.0, resource.Radiuses[1]); + } + + [Test] + public void GetUrl_ReturnsCorrectBaseUrl() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("directions/v5/")); + Assert.IsTrue(url.Contains("mapbox/driving/")); + Assert.IsTrue(url.EndsWith(".json")); + } + + [Test] + public void GetUrl_IncludesCoordinatesInCorrectFormat() + { + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + var url = resource.GetUrl(); + + // Coordinates should be in lon,lat format separated by semicolons + Assert.IsTrue(url.Contains("-122")); + Assert.IsTrue(url.Contains("37")); + Assert.IsTrue(url.Contains(";")); + } + + [Test] + public void GetUrl_IncludesAlternativesWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Alternatives = true; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("alternatives=true")); + } + + [Test] + public void GetUrl_IncludesStepsWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Steps = true; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("steps=true")); + } + + [Test] + public void GetUrl_IncludesContinueStraightWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.ContinueStraight = false; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("continue_straight=false")); + } + + [Test] + public void GetUrl_IncludesOverviewWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Overview = Overview.Full; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("overview=full")); + } + + [Test] + public void GetUrl_IncludesRadiusesWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Radiuses = new double[] { 100.0, 200.0 }; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("radiuses=")); + } + + [Test] + public void GetUrl_IncludesBearingsWhenSet() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + resource.Bearings = new BearingFilter[] + { + new BearingFilter(45, 10), + new BearingFilter(90, 20) + }; + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("bearings=")); + } + + [Test] + public void GetUrl_CombinesMultipleParameters() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Walking); + resource.Alternatives = true; + resource.Steps = true; + resource.Overview = Overview.Simplified; + + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("alternatives=true")); + Assert.IsTrue(url.Contains("steps=true")); + Assert.IsTrue(url.Contains("overview=simplified")); + } + + [Test] + public void GetUrl_IncludesDrivingProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Driving); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("mapbox/driving/")); + } + + [Test] + public void GetUrl_IncludesWalkingProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Walking); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("mapbox/walking/")); + } + + [Test] + public void GetUrl_IncludesCyclingProfile() + { + var resource = new DirectionResource(_validCoordinates, RoutingProfile.Cycling); + var url = resource.GetUrl(); + + Assert.IsTrue(url.Contains("mapbox/cycling/")); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta new file mode 100644 index 000000000..ccca4d240 --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionResourceTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd480d299eaa54556b8fd4146a936e87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs new file mode 100644 index 000000000..bd6cabb1f --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.BaseModule.Utilities.JsonConverters; +using Mapbox.DirectionsApi.Response; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class DirectionsResponseTests + { + private string _sampleResponseJson = @"{ + ""routes"": [ + { + ""distance"": 12345.6, + ""duration"": 789.0, + ""weight"": 800.0 + } + ], + ""waypoints"": [ + { + ""name"": ""Main Street"", + ""location"": [-122.4194, 37.7749] + }, + { + ""name"": ""Market Street"", + ""location"": [-118.2437, 34.0522] + } + ], + ""code"": ""Ok"" + }"; + + [Test] + public void Deserialize_ParsesValidResponse() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.IsNotNull(response); + Assert.AreEqual("Ok", response.Code); + } + + [Test] + public void Deserialize_ParsesRoutes() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.IsNotNull(response.Routes); + Assert.AreEqual(1, response.Routes.Count); + Assert.AreEqual(12345.6, response.Routes[0].Distance); + Assert.AreEqual(789.0, response.Routes[0].Duration); + } + + [Test] + public void Deserialize_ParsesWaypoints() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.IsNotNull(response.Waypoints); + Assert.AreEqual(2, response.Waypoints.Count); + Assert.AreEqual("Main Street", response.Waypoints[0].Name); + Assert.AreEqual("Market Street", response.Waypoints[1].Name); + } + + [Test] + public void Deserialize_ParsesCode() + { + var response = JsonConvert.DeserializeObject(_sampleResponseJson, JsonConverters.Converters); + + Assert.AreEqual("Ok", response.Code); + } + + [Test] + public void Serialize_CreatesValidJson() + { + var response = new DirectionsResponse + { + Code = "Ok", + Routes = new List(), + Waypoints = new List() + }; + + var json = JsonConvert.SerializeObject(response, JsonConverters.Converters); + + Assert.IsNotNull(json); + Assert.IsTrue(json.Contains("\"code\"")); + Assert.IsTrue(json.Contains("\"routes\"")); + Assert.IsTrue(json.Contains("\"waypoints\"")); + } + + [Test] + public void Deserialize_HandlesEmptyRoutes() + { + var json = @"{ + ""routes"": [], + ""waypoints"": [], + ""code"": ""NoRoute"" + }"; + + var response = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.IsNotNull(response.Routes); + Assert.AreEqual(0, response.Routes.Count); + Assert.AreEqual("NoRoute", response.Code); + } + + [Test] + public void Deserialize_HandlesMultipleRoutes() + { + var json = @"{ + ""routes"": [ + { + ""distance"": 1000.0, + ""duration"": 100.0, + ""weight"": 110.0 + }, + { + ""distance"": 1200.0, + ""duration"": 120.0, + ""weight"": 130.0 + } + ], + ""waypoints"": [], + ""code"": ""Ok"" + }"; + + var response = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.AreEqual(2, response.Routes.Count); + Assert.AreEqual(1000.0, response.Routes[0].Distance); + Assert.AreEqual(1200.0, response.Routes[1].Distance); + } + + [Test] + public void SerializeDeserialize_RoundTrip() + { + var original = new DirectionsResponse + { + Code = "Ok", + Routes = new List + { + new Route + { + Distance = 5000.0, + Duration = 300.0, + Weight = 320.0f, + Geometry = new List() + } + }, + Waypoints = new List + { + new Waypoint + { + Name = "Test Waypoint" + } + } + }; + + var json = JsonConvert.SerializeObject(original, JsonConverters.Converters); + var deserialized = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.AreEqual(original.Code, deserialized.Code); + Assert.AreEqual(original.Routes.Count, deserialized.Routes.Count); + Assert.AreEqual(original.Routes[0].Distance, deserialized.Routes[0].Distance); + Assert.AreEqual(original.Waypoints[0].Name, deserialized.Waypoints[0].Name); + } + + [Test] + public void Deserialize_HandlesNullFields() + { + var json = @"{ + ""code"": ""Ok"" + }"; + + var response = JsonConvert.DeserializeObject(json, JsonConverters.Converters); + + Assert.IsNotNull(response); + Assert.AreEqual("Ok", response.Code); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta new file mode 100644 index 000000000..1d3c79944 --- /dev/null +++ b/Tests/Editor/DirectionsApi/DirectionsResponseTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6ba3a82c9b974826a8a3a7da34ea5d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef new file mode 100644 index 000000000..15be2e44d --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef @@ -0,0 +1,26 @@ +{ + "name": "MapboxDirectionsApiTests", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:8ba268152fd3143f59445711358cb12f" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Newtonsoft.Json.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta new file mode 100644 index 000000000..b9cdd95dc --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 917423ea424014119b14334842c37791 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs new file mode 100644 index 000000000..c63ff14c7 --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs @@ -0,0 +1,308 @@ +using System; +using System.Text; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.DirectionsApi; +using Mapbox.DirectionsApi.Response; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class MapboxDirectionsApiTests + { + private class MockFileSource : IFileSource + { + private readonly string _responseJson; + private readonly bool _shouldError; + + public MockFileSource(string responseJson, bool shouldError = false) + { + _responseJson = responseJson; + _shouldError = shouldError; + } + + public IAsyncRequest Request(string uri, Action callback, int timeout = 10) + { + if (!_shouldError) + { + var response = new Response + { + Data = Encoding.UTF8.GetBytes(_responseJson) + }; + callback(response); + } + + return new MockAsyncRequest(); + } + + public IWebRequest MapboxImageRequest(string uri, Action callback, string etag = "", int timeout = 10, bool isNonReadable = true) + { + throw new NotImplementedException("Not needed for Directions API tests"); + } + + public IWebRequest CustomImageRequest(string uri, Action callback, string etag = null, int timeout = 10, bool isNonReadable = true) + { + throw new NotImplementedException("Not needed for Directions API tests"); + } + + public IWebRequest MapboxDataRequest(string uri, Action callback, string etag = "", int timeout = 10) + { + throw new NotImplementedException("Not needed for Directions API tests"); + } + + public void OnDestroy() + { + // No cleanup needed for mock + } + } + + private class MockAsyncRequest : IAsyncRequest + { + public bool IsCompleted { get; set; } + public HttpRequestType RequestType { get; set; } + + public void Cancel() + { + IsCompleted = true; + } + } + + private string _validResponseJson = @"{ + ""routes"": [ + { + ""distance"": 10000.0, + ""duration"": 500.0, + ""weight"": 520.0 + } + ], + ""waypoints"": [ + { + ""name"": ""Start Point"", + ""location"": [-122.4194, 37.7749] + }, + { + ""name"": ""End Point"", + ""location"": [-118.2437, 34.0522] + } + ], + ""code"": ""Ok"" + }"; + + [Test] + public void Constructor_AcceptsFileSource() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + Assert.IsNotNull(api); + } + + [Test] + public void Query_ExecutesCallback() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + } + + [Test] + public void Query_ReturnsValidResponse() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + Assert.AreEqual("Ok", capturedResponse.Code); + Assert.AreEqual(1, capturedResponse.Routes.Count); + Assert.AreEqual(2, capturedResponse.Waypoints.Count); + } + + [Test] + public void Query_ParsesRouteData() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.AreEqual(10000.0, capturedResponse.Routes[0].Distance); + Assert.AreEqual(500.0, capturedResponse.Routes[0].Duration); + Assert.AreEqual(520.0, capturedResponse.Routes[0].Weight); + } + + [Test] + public void Query_ParsesWaypointData() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.AreEqual("Start Point", capturedResponse.Waypoints[0].Name); + Assert.AreEqual("End Point", capturedResponse.Waypoints[1].Name); + } + + [Test] + public void Query_ReturnsAsyncRequest() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + var asyncRequest = api.Query(resource, response => { }); + + Assert.IsNotNull(asyncRequest); + } + + [Test] + public void Serialize_CreatesJson() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var response = new DirectionsResponse + { + Code = "Ok", + Routes = new System.Collections.Generic.List(), + Waypoints = new System.Collections.Generic.List() + }; + + var json = api.Serialize(response); + + Assert.IsNotNull(json); + Assert.IsTrue(json.Length > 0); + } + + [Test] + public void Query_HandlesDifferentProfiles() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + + // Test with different routing profiles + var walkingResource = new DirectionResource(coords, RoutingProfile.Walking); + DirectionsResponse walkingResponse = null; + api.Query(walkingResource, response => { walkingResponse = response; }); + + Assert.IsNotNull(walkingResponse); + Assert.AreEqual("Ok", walkingResponse.Code); + } + + [Test] + public void Query_HandlesEmptyRoutes() + { + var emptyRoutesJson = @"{ + ""routes"": [], + ""waypoints"": [], + ""code"": ""NoRoute"" + }"; + + var mockSource = new MockFileSource(emptyRoutesJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + Assert.AreEqual("NoRoute", capturedResponse.Code); + Assert.AreEqual(0, capturedResponse.Routes.Count); + } + + [Test] + public void Query_HandlesMultipleWaypoints() + { + var mockSource = new MockFileSource(_validResponseJson); + var api = new MapboxDirectionsApi(mockSource); + + var coords = new LatitudeLongitude[] + { + new LatitudeLongitude(37.7749, -122.4194), + new LatitudeLongitude(36.1699, -115.1398), // Las Vegas + new LatitudeLongitude(34.0522, -118.2437) + }; + var resource = new DirectionResource(coords, RoutingProfile.Driving); + + DirectionsResponse capturedResponse = null; + api.Query(resource, response => + { + capturedResponse = response; + }); + + Assert.IsNotNull(capturedResponse); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta new file mode 100644 index 000000000..87cbc4a06 --- /dev/null +++ b/Tests/Editor/DirectionsApi/MapboxDirectionsApiTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19233547ac7754529b553582332d9a05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/OverviewTests.cs b/Tests/Editor/DirectionsApi/OverviewTests.cs new file mode 100644 index 000000000..226caf110 --- /dev/null +++ b/Tests/Editor/DirectionsApi/OverviewTests.cs @@ -0,0 +1,65 @@ +using Mapbox.DirectionsApi; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class OverviewTests + { + [Test] + public void Full_Overview_Exists() + { + Assert.IsNotNull(Overview.Full); + } + + [Test] + public void Simplified_Overview_Exists() + { + Assert.IsNotNull(Overview.Simplified); + } + + [Test] + public void False_Overview_Exists() + { + Assert.IsNotNull(Overview.False); + } + + [Test] + public void Full_ToString_ReturnsCorrectValue() + { + var overview = Overview.Full; + Assert.AreEqual("full", overview.ToString()); + } + + [Test] + public void Simplified_ToString_ReturnsCorrectValue() + { + var overview = Overview.Simplified; + Assert.AreEqual("simplified", overview.ToString()); + } + + [Test] + public void False_ToString_ReturnsCorrectValue() + { + var overview = Overview.False; + Assert.AreEqual("false", overview.ToString()); + } + + [Test] + public void Overviews_AreUnique() + { + Assert.AreNotEqual(Overview.Full, Overview.Simplified); + Assert.AreNotEqual(Overview.Full, Overview.False); + Assert.AreNotEqual(Overview.Simplified, Overview.False); + } + + [Test] + public void Overview_IsSameInstance() + { + var full1 = Overview.Full; + var full2 = Overview.Full; + + Assert.AreSame(full1, full2); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/OverviewTests.cs.meta b/Tests/Editor/DirectionsApi/OverviewTests.cs.meta new file mode 100644 index 000000000..278b3ed0b --- /dev/null +++ b/Tests/Editor/DirectionsApi/OverviewTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9161c8d094e3499c98681b987bbffb4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/DirectionsApi/RoutingProfileTests.cs b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs new file mode 100644 index 000000000..0933ef984 --- /dev/null +++ b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs @@ -0,0 +1,86 @@ +using Mapbox.DirectionsApi; +using NUnit.Framework; + +namespace Mapbox.DirectionsApiTests +{ + [TestFixture] + public class RoutingProfileTests + { + [Test] + public void Driving_Profile_Exists() + { + Assert.IsNotNull(RoutingProfile.Driving); + } + + [Test] + public void Walking_Profile_Exists() + { + Assert.IsNotNull(RoutingProfile.Walking); + } + + [Test] + public void Cycling_Profile_Exists() + { + Assert.IsNotNull(RoutingProfile.Cycling); + } + + [Test] + public void Driving_ToString_ReturnsCorrectValue() + { + var profile = RoutingProfile.Driving; + Assert.AreEqual("mapbox/driving/", profile.ToString()); + } + + [Test] + public void Walking_ToString_ReturnsCorrectValue() + { + var profile = RoutingProfile.Walking; + Assert.AreEqual("mapbox/walking/", profile.ToString()); + } + + [Test] + public void Cycling_ToString_ReturnsCorrectValue() + { + var profile = RoutingProfile.Cycling; + Assert.AreEqual("mapbox/cycling/", profile.ToString()); + } + + [Test] + public void GetProfile_ReturnsDriving_ForDrivingOption() + { + var profile = RoutingProfile.GetProfile(RoutingProfile.RoutingProfileOptions.Driving); + Assert.AreEqual(RoutingProfile.Driving, profile); + } + + [Test] + public void GetProfile_ReturnsWalking_ForWalkingOption() + { + var profile = RoutingProfile.GetProfile(RoutingProfile.RoutingProfileOptions.Walking); + Assert.AreEqual(RoutingProfile.Walking, profile); + } + + [Test] + public void GetProfile_ReturnsCycling_ForCyclingOption() + { + var profile = RoutingProfile.GetProfile(RoutingProfile.RoutingProfileOptions.Cycling); + Assert.AreEqual(RoutingProfile.Cycling, profile); + } + + [Test] + public void Profiles_AreUnique() + { + Assert.AreNotEqual(RoutingProfile.Driving, RoutingProfile.Walking); + Assert.AreNotEqual(RoutingProfile.Driving, RoutingProfile.Cycling); + Assert.AreNotEqual(RoutingProfile.Walking, RoutingProfile.Cycling); + } + + [Test] + public void Profile_IsSameInstance() + { + var driving1 = RoutingProfile.Driving; + var driving2 = RoutingProfile.Driving; + + Assert.AreSame(driving1, driving2); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta new file mode 100644 index 000000000..cd04e70b2 --- /dev/null +++ b/Tests/Editor/DirectionsApi/RoutingProfileTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efb0bce33a835455fa36122db3e142f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule.meta b/Tests/Editor/LocationModule.meta new file mode 100644 index 000000000..d8976a4e5 --- /dev/null +++ b/Tests/Editor/LocationModule.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3ae875080440846aca4a90a79f1d128a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/LocationDataTests.cs b/Tests/Editor/LocationModule/LocationDataTests.cs new file mode 100644 index 000000000..b04bd0213 --- /dev/null +++ b/Tests/Editor/LocationModule/LocationDataTests.cs @@ -0,0 +1,264 @@ +using Mapbox.BaseModule.Data.Vector2d; +using Mapbox.LocationModule; +using NUnit.Framework; + +namespace Mapbox.LocationModule.Tests +{ + [TestFixture] + public class LocationDataTests + { + #region Location Tests + + [Test] + public void Location_DefaultConstructor_InitializesWithDefaults() + { + // Act - Location is a struct, so default constructor sets all fields to default values + var location = new Location(); + + // Assert + Assert.AreEqual(0, location.LatitudeLongitude.Latitude); + Assert.AreEqual(0, location.LatitudeLongitude.Longitude); + Assert.AreEqual(0f, location.Accuracy); + Assert.AreEqual(0.0, location.Timestamp); + Assert.IsFalse(location.IsLocationUpdated); + Assert.IsFalse(location.IsLocationServiceEnabled); + } + + [Test] + public void Location_SetLatitudeLongitude_UpdatesValue() + { + // Arrange + var location = new Location(); + var latLng = new LatitudeLongitude(37.7749, -122.4194); + + // Act + location.LatitudeLongitude = latLng; + + // Assert + Assert.AreEqual(37.7749, location.LatitudeLongitude.Latitude, 0.0001); + Assert.AreEqual(-122.4194, location.LatitudeLongitude.Longitude, 0.0001); + } + + [Test] + public void Location_Accuracy_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.Accuracy = 15.5f; + + // Assert + Assert.AreEqual(15.5f, location.Accuracy); + } + + [Test] + public void Location_Timestamp_CanBeSet() + { + // Arrange + var location = new Location(); + double timestamp = 1234567890.0; + + // Act + location.Timestamp = timestamp; + + // Assert + Assert.AreEqual(timestamp, location.Timestamp); + } + + [Test] + public void Location_IsLocationUpdated_DefaultsToFalse() + { + // Arrange & Act + var location = new Location(); + + // Assert + Assert.IsFalse(location.IsLocationUpdated); + } + + [Test] + public void Location_IsLocationServiceEnabled_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.IsLocationServiceEnabled = true; + + // Assert + Assert.IsTrue(location.IsLocationServiceEnabled); + } + + [Test] + public void Location_UserHeading_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.UserHeading = 45.5f; + + // Assert + Assert.AreEqual(45.5f, location.UserHeading); + } + + [Test] + public void Location_DeviceOrientation_CanBeSet() + { + // Arrange + var location = new Location(); + + // Act + location.DeviceOrientation = 90.0f; + + // Assert + Assert.AreEqual(90.0f, location.DeviceOrientation); + } + + #endregion + + #region LatitudeLongitude Tests + + [Test] + public void LatitudeLongitude_Constructor_SetsValues() + { + // Act + var latLng = new LatitudeLongitude(37.7749, -122.4194); + + // Assert + Assert.AreEqual(37.7749, latLng.Latitude, 0.0001); + Assert.AreEqual(-122.4194, latLng.Longitude, 0.0001); + } + + [Test] + public void LatitudeLongitude_Equals_SameValues_ReturnsTrue() + { + // Arrange + var latLng1 = new LatitudeLongitude(37.7749, -122.4194); + var latLng2 = new LatitudeLongitude(37.7749, -122.4194); + + // Act & Assert + Assert.That(latLng1.Equals(latLng2), Is.True); + } + + [Test] + public void LatitudeLongitude_Equals_DifferentLatitude_ReturnsFalse() + { + // Arrange + var latLng1 = new LatitudeLongitude(37.7749, -122.4194); + var latLng2 = new LatitudeLongitude(37.7750, -122.4194); + + // Act & Assert + Assert.That(latLng1.Equals(latLng2), Is.False); + } + + [Test] + public void LatitudeLongitude_Equals_DifferentLongitude_ReturnsFalse() + { + // Arrange + var latLng1 = new LatitudeLongitude(37.7749, -122.4194); + var latLng2 = new LatitudeLongitude(37.7749, -122.4195); + + // Act & Assert + Assert.That(latLng1.Equals(latLng2), Is.False); + } + + [Test] + public void LatitudeLongitude_ZeroZero_IsValid() + { + // Act - 0,0 is Gulf of Guinea, a valid location + var latLng = new LatitudeLongitude(0, 0); + + // Assert + Assert.AreEqual(0, latLng.Latitude, 0.0001); + Assert.AreEqual(0, latLng.Longitude, 0.0001); + Assert.That(latLng.IsValid(), Is.True); + } + + [Test] + public void LatitudeLongitude_ExtremeValues_AreValid() + { + // Act - Test near poles and date line + var northPole = new LatitudeLongitude(90, 0); + var southPole = new LatitudeLongitude(-90, 0); + var dateLineWest = new LatitudeLongitude(0, -180); + var dateLineEast = new LatitudeLongitude(0, 180); + + // Assert + Assert.AreEqual(90, northPole.Latitude, 0.0001); + Assert.AreEqual(-90, southPole.Latitude, 0.0001); + Assert.AreEqual(-180, dateLineWest.Longitude, 0.0001); + Assert.AreEqual(180, dateLineEast.Longitude, 0.0001); + + Assert.That(northPole.IsValid(), Is.True); + Assert.That(southPole.IsValid(), Is.True); + Assert.That(dateLineWest.IsValid(), Is.True); + Assert.That(dateLineEast.IsValid(), Is.True); + } + + #endregion + + #region StaticLocationProvider Tests + + [Test] + public void StaticLocationProvider_StringConstructor_ParsesValidCoordinates() + { + // Arrange & Act + var provider = new StaticLocationProvider("37.7749,-122.4194"); + + // Assert + Assert.AreEqual(37.7749, provider.CurrentLocation.LatitudeLongitude.Latitude, 0.001); + Assert.AreEqual(-122.4194, provider.CurrentLocation.LatitudeLongitude.Longitude, 0.001); + } + + [Test] + public void StaticLocationProvider_LatLngConstructor_SetsLocation() + { + // Arrange & Act + var latLng = new LatitudeLongitude(37.7749, -122.4194); + var provider = new StaticLocationProvider(latLng); + + // Assert + Assert.AreEqual(37.7749, provider.CurrentLocation.LatitudeLongitude.Latitude, 0.001); + Assert.AreEqual(-122.4194, provider.CurrentLocation.LatitudeLongitude.Longitude, 0.001); + } + + [Test] + public void StaticLocationProvider_AfterSendLocationEvent_IsEnabled() + { + // Arrange + var provider = new StaticLocationProvider("37.7749,-122.4194"); + + // Act + provider.SendLocationEvent(); + + // Assert + Assert.That(provider.CurrentLocation.IsLocationServiceEnabled, Is.True); + } + + #endregion + + #region ILocationProvider Tests + + [Test] + public void AbstractLocationProvider_OnLocationUpdated_EventCanBeSubscribed() + { + // Arrange + var provider = new StaticLocationProvider("37.7749,-122.4194"); + bool eventRaised = false; + + provider.OnLocationUpdated += (location) => + { + eventRaised = true; + }; + + // Act - StaticLocationProvider requires SendLocationEvent() to trigger the event + provider.SendLocationEvent(); + + // Assert + Assert.That(eventRaised, Is.True); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/LocationDataTests.cs.meta b/Tests/Editor/LocationModule/LocationDataTests.cs.meta new file mode 100644 index 000000000..daecf38dd --- /dev/null +++ b/Tests/Editor/LocationModule/LocationDataTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0944954b179ed4f6cb5102f72228f3b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/LocationPermissionTests.cs b/Tests/Editor/LocationModule/LocationPermissionTests.cs new file mode 100644 index 000000000..92c350052 --- /dev/null +++ b/Tests/Editor/LocationModule/LocationPermissionTests.cs @@ -0,0 +1,78 @@ +using Mapbox.BaseModule.Unity; +using NUnit.Framework; + +namespace Mapbox.LocationModule.Tests +{ + [TestFixture] + public class LocationPermissionTests + { + #region LocationPermissionState Tests + + [Test] + public void LocationPermissionState_HasExpectedValues() + { + // Assert - Verify enum has all expected values + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.Waiting)); + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.Granted)); + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.Denied)); + Assert.IsTrue(System.Enum.IsDefined(typeof(LocationPermissionState), LocationPermissionState.DeniedPermanently)); + } + + [Test] + public void LocationPermissionState_DeniedAndDeniedPermanentlyAreDifferent() + { + // Assert + Assert.AreNotEqual( + LocationPermissionState.Denied, + LocationPermissionState.DeniedPermanently, + "Denied and DeniedPermanently should be distinct states"); + } + + #endregion + + #region UnityContext Tests + + [Test] + public void UnityContext_DefaultState_IsWaiting() + { + // Arrange & Act + var context = new UnityContext(); + + // Assert + Assert.AreEqual(LocationPermissionState.Waiting, context.LocationPermissionState); + } + + [Test] + public void UnityContext_MapRootCanBeSet() + { + // Arrange + var context = new UnityContext(); + var gameObject = new UnityEngine.GameObject("TestMapRoot"); + + // Act + context.MapRoot = gameObject.transform; + + // Assert + Assert.AreEqual(gameObject.transform, context.MapRoot); + + // Cleanup + UnityEngine.Object.DestroyImmediate(gameObject); + } + + #endregion + + #region LocationPermissionHandler Tests + + [Test] + public void LocationPermissionHandler_InitialState_IsWaiting() + { + // Arrange & Act + var handler = new LocationPermissionHandler(); + + // Assert + Assert.AreEqual(LocationPermissionState.Waiting, handler.State); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/LocationPermissionTests.cs.meta b/Tests/Editor/LocationModule/LocationPermissionTests.cs.meta new file mode 100644 index 000000000..d19db1131 --- /dev/null +++ b/Tests/Editor/LocationModule/LocationPermissionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e29407a534d9455bb5ba68d816f944d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef new file mode 100644 index 000000000..57eb210df --- /dev/null +++ b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef @@ -0,0 +1,21 @@ +{ + "name": "Mapbox.LocationModule.Tests", + "rootNamespace": "Mapbox.LocationModule.Tests", + "references": [ + "MapboxBaseModule", + "MapboxLocationModule" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta new file mode 100644 index 000000000..77742fc8d --- /dev/null +++ b/Tests/Editor/LocationModule/Mapbox.LocationModule.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3e6a21ab0cdd24a259d184a293c05435 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs new file mode 100644 index 000000000..c8c72e7ec --- /dev/null +++ b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs @@ -0,0 +1,195 @@ +using Mapbox.LocationModule.MapboxLocation; +using Mapbox.LocationModule; +using NUnit.Framework; +using System; + +namespace Mapbox.LocationModule.Tests +{ + [TestFixture] + public class MapboxLocationProviderTests + { + private MapboxLocationSettings _settings; + + [SetUp] + public void SetUp() + { + _settings = new MapboxLocationSettings + { + AccuracyLevel = MapboxLocationAccuracyLevel.High, + Displacement = 10f, + Interval = 1000, + MinimumInterval = 500, + MaximumInterval = 5000 + }; + } + + #region Constructor Tests + + [Test] + public void Constructor_WithValidSettings_CreatesProvider() + { + // Act + var provider = new MapboxLocationProvider(_settings); + + // Assert + Assert.IsNotNull(provider); + } + + [Test] + public void Constructor_InEditor_DoesNotThrow() + { + // In editor, _mapboxDeviceLocation will be null since platform #if guards prevent instantiation + // The null guard added in our fix should prevent crashes + // Act & Assert + Assert.DoesNotThrow(() => new MapboxLocationProvider(_settings)); + } + + #endregion + + #region Update Tests + + [Test] + public void Update_WithNullDeviceLocation_DoesNotThrow() + { + // Arrange + var provider = new MapboxLocationProvider(_settings); + // In editor, _mapboxDeviceLocation is null + + // Act & Assert + Assert.DoesNotThrow(() => provider.Update()); + } + + #endregion + + #region OnDestroy Tests + + [Test] + public void OnDestroy_WithNullDeviceLocation_DoesNotThrow() + { + // Arrange + var provider = new MapboxLocationProvider(_settings); + // In editor, _mapboxDeviceLocation is null + + // Act & Assert + Assert.DoesNotThrow(() => provider.OnDestroy()); + } + + #endregion + + #region CurrentLocation Tests + + [Test] + public void CurrentLocation_InitialState_ReturnsDefaultLocation() + { + // Arrange + var provider = new MapboxLocationProvider(_settings); + + // Act + var location = provider.CurrentLocation; + + // Assert + Assert.AreEqual(0, location.LatitudeLongitude.Latitude); + Assert.AreEqual(0, location.LatitudeLongitude.Longitude); + } + + #endregion + } + + [TestFixture] + public class MapboxLocationSettingsTests + { + #region Settings Tests + + [Test] + public void MapboxLocationSettings_DefaultValues() + { + // Act + var settings = new MapboxLocationSettings(); + + // Assert + Assert.IsNotNull(settings); + } + + [Test] + public void MapboxLocationSettings_AccuracyLevel_CanBeSet() + { + // Arrange + var settings = new MapboxLocationSettings(); + + // Act + settings.AccuracyLevel = MapboxLocationAccuracyLevel.Highest; + + // Assert + Assert.AreEqual(MapboxLocationAccuracyLevel.Highest, settings.AccuracyLevel); + } + + [Test] + public void MapboxLocationSettings_Displacement_CanBeSet() + { + // Arrange + var settings = new MapboxLocationSettings(); + + // Act + settings.Displacement = 25.5f; + + // Assert + Assert.AreEqual(25.5f, settings.Displacement); + } + + #endregion + } + + [TestFixture] + public class AccuracyAuthorizationTests + { + #region Enum Tests + + [Test] + public void AccuracyAuthorization_HasExpectedValues() + { + // Assert - Verify enum has expected values + Assert.That(Enum.IsDefined(typeof(AccuracyAuthorization), AccuracyAuthorization.None), Is.True); + Assert.That(Enum.IsDefined(typeof(AccuracyAuthorization), AccuracyAuthorization.Exact), Is.True); + Assert.That(Enum.IsDefined(typeof(AccuracyAuthorization), AccuracyAuthorization.Inexact), Is.True); + } + + #endregion + } + + [TestFixture] + public class MapboxLocationServiceStatusTests + { + #region Enum Tests + + [Test] + public void MapboxLocationServiceStatus_HasExpectedValues() + { + // Assert - Verify enum has expected status values + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Denied), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Granted), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Foreground), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationServiceStatus), MapboxLocationServiceStatus.Background), Is.True); + } + + #endregion + } + + [TestFixture] + public class MapboxLocationAccuracyLevelTests + { + #region Enum Tests + + [Test] + public void MapboxLocationAccuracyLevel_HasExpectedValues() + { + // Assert - Verify enum has all accuracy levels + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Passive), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Low), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Medium), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.High), Is.True); + Assert.That(Enum.IsDefined(typeof(MapboxLocationAccuracyLevel), MapboxLocationAccuracyLevel.Highest), Is.True); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta new file mode 100644 index 000000000..72d7dffcc --- /dev/null +++ b/Tests/Editor/LocationModule/MapboxLocationProviderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b75e9cdda7fd94e2fb2d85335fb3fd15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/TestLocationServiceMock.cs b/Tests/Editor/LocationModule/TestLocationServiceMock.cs new file mode 100644 index 000000000..741483c24 --- /dev/null +++ b/Tests/Editor/LocationModule/TestLocationServiceMock.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Mapbox.LocationModule.UnityLocationWrappers; +using UnityEngine; + +namespace Mapbox.LocationModule.Tests +{ + /// + /// Simple mock location service for unit tests. + /// Allows queueing specific location data to be returned on demand. + /// + public class TestLocationServiceMock : IMapboxLocationService + { + private readonly Queue _locations = new Queue(); + private IMapboxLocationInfo _currentLocation; + private LocationServiceStatus _status = LocationServiceStatus.Stopped; + private bool _isEnabledByUser = true; + + public bool isEnabledByUser => _isEnabledByUser; + public LocationServiceStatus status => _status; + public IMapboxLocationInfo lastData => _currentLocation; + + public void SetEnabledByUser(bool enabled) + { + _isEnabledByUser = enabled; + } + + public void SetStatus(LocationServiceStatus newStatus) + { + _status = newStatus; + } + + public void QueueLocation(IMapboxLocationInfo location) + { + _locations.Enqueue(location); + } + + public void QueueLocation(float latitude, float longitude, float accuracy = 10f, double timestamp = 0) + { + var location = new TestLocationInfo + { + latitude = latitude, + longitude = longitude, + accuracy = accuracy, + timestamp = timestamp > 0 ? timestamp : System.DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + _locations.Enqueue(location); + } + + public void Start(float desiredAccuracyInMeters, float updateDistanceInMeters) + { + _status = LocationServiceStatus.Running; + AdvanceToNextLocation(); + } + + public void Stop() + { + _status = LocationServiceStatus.Stopped; + } + + /// + /// Simulates location service update by advancing to next queued location. + /// + public void AdvanceToNextLocation() + { + if (_locations.Count > 0) + { + _currentLocation = _locations.Dequeue(); + } + } + } + + /// + /// Simple test implementation of IMapboxLocationInfo. + /// + public class TestLocationInfo : IMapboxLocationInfo + { + public float latitude { get; set; } + public float longitude { get; set; } + public float altitude { get; set; } + public float horizontalAccuracy { get; set; } + public float verticalAccuracy { get; set; } + public float accuracy + { + get => horizontalAccuracy; + set => horizontalAccuracy = value; + } + public double timestamp { get; set; } + } +} diff --git a/Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta b/Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta new file mode 100644 index 000000000..32a9f5b3e --- /dev/null +++ b/Tests/Editor/LocationModule/TestLocationServiceMock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78ad45c17decb455799434c9e672ab23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/LocationModule/UnityLocationProviderTests.cs b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs new file mode 100644 index 000000000..c61b78753 --- /dev/null +++ b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs @@ -0,0 +1,88 @@ +using Mapbox.LocationModule; +using Mapbox.LocationModule.AngleSmoothing; +using NUnit.Framework; + +namespace Mapbox.LocationModule.Tests +{ + /// + /// NOTE: UnityLocationProvider requires Unity's runtime (MonoBehaviour, coroutines) to function. + /// These EditMode tests are limited to testing settings and data structures. + /// Full integration tests for UnityLocationProvider would need to be PlayMode tests. + /// + [TestFixture] + public class UnityLocationProviderTests + { + #region Settings Tests + + [Test] + public void UnityLocationProviderSettings_DefaultValues() + { + // Act + var settings = new UnityLocationProviderSettings(); + + // Assert + Assert.IsNotNull(settings); + Assert.AreEqual(1.0f, settings.DesiredAccuracyInMeters); + Assert.AreEqual(0.0f, settings.UpdateDistanceInMeters); + Assert.AreEqual(500, settings.UpdateTimeInMilliSeconds); + } + + [Test] + public void UnityLocationProviderSettings_CanSetDesiredAccuracy() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + + // Act + settings.DesiredAccuracyInMeters = 25.5f; + + // Assert + Assert.AreEqual(25.5f, settings.DesiredAccuracyInMeters); + } + + [Test] + public void UnityLocationProviderSettings_CanSetUpdateDistance() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + + // Act + settings.UpdateDistanceInMeters = 10.0f; + + // Assert + Assert.AreEqual(10.0f, settings.UpdateDistanceInMeters); + } + + [Test] + public void UnityLocationProviderSettings_CanSetUpdateTime() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + + // Act + settings.UpdateTimeInMilliSeconds = 1000; + + // Assert + Assert.AreEqual(1000, settings.UpdateTimeInMilliSeconds); + } + + [Test] + public void UnityLocationProviderSettings_CanSetSmoothingStrategies() + { + // Arrange + var settings = new UnityLocationProviderSettings(); + var headingSmoothing = new AngleSmoothingNoOp(); + var orientationSmoothing = new AngleSmoothingNoOp(); + + // Act + settings.UserHeadingSmoothing = headingSmoothing; + settings.DeviceOrientationSmoothing = orientationSmoothing; + + // Assert + Assert.AreEqual(headingSmoothing, settings.UserHeadingSmoothing); + Assert.AreEqual(orientationSmoothing, settings.DeviceOrientationSmoothing); + } + + #endregion + } +} diff --git a/Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta new file mode 100644 index 000000000..1c44bb83f --- /dev/null +++ b/Tests/Editor/LocationModule/UnityLocationProviderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01bc1970791b14611b4142d513dcc95a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs new file mode 100644 index 000000000..07454d942 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs @@ -0,0 +1,217 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.MeshGeneration; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using Mapbox.VectorModule.Unity; +using NUnit.Framework; +using Unity.PerformanceTesting; +using UnityEditor; +using UnityEngine.TestTools; + +namespace Mapbox.VectorModuleTests +{ + public class BuildingLayerPerformanceTests + { + private int SampleCount = 100; + + private ResilientWebRequestFileSource _fs; + private byte[] buffer; + private CanonicalTileId tileId = new CanonicalTileId(15, 18654, 9481); + private string LatLng = "60.1664427,24.9318587"; + private string _buildingVisualizerAssetPath = "Packages/com.mapbox.sdk/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset"; + private string _oldBuildVisAssetPath = "Packages/com.mapbox.sdk/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset"; + + [UnitySetUp] + public IEnumerator SetUp() + { + var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); + _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + + Response response = null; + var req = _fs.Request("https://api.mapbox.com/v4/mapbox.mapbox-streets-v7/" + tileId + ".vector.pbf", (r) => + { + response = r; + }); + while (!req.IsCompleted) + { + yield return null; + } + + buffer = response.Data; + Assert.NotZero(buffer.Length); + } + + private IMapInformation GetMapInformation() + { + var mapInformation = new MapInformation(LatLng); + mapInformation.SetInformation(null, 16, 0, 0, 1000); + mapInformation.Initialize(); + return mapInformation; + } + + private BuildingComponentVisualizer BuildingLayer(IMapInformation mapInformation) + { + var viz = new BuildingComponentVisualizer("test", mapInformation, null, null); + viz.Initialize(); + return viz; + } + + private VectorLayerVisualizer RegularLayer(IMapInformation mapInformation, bool doChamfer) + { + var viz = new VectorLayerVisualizer("test", mapInformation, null, null); + var modStackSettings = new ModifierStackSettings() { MergeObjects = true }; + var modStack = new ModifierStack(modStackSettings, null); + modStack.MeshModifiers.Add(new SnapTerrainModifier()); + modStack.MeshModifiers.Add(new PolygonMeshModifier(0)); + if (doChamfer) + { + modStack.MeshModifiers.Add(new ChamferHeightModifier(new ChamferModifierSettings() + { FlatTops = true, OffsetInMeters = 1 })); + } + else + { + modStack.MeshModifiers.Add(new HeightModifier(new GeometryExtrusionOptions())); + } + + viz.AddModifierStack(new List() { modStack }); + viz.Initialize(); + return viz; + } + + private VectorLayerVisualizer RegularRoadLayer(IMapInformation mapInformation) + { + var viz = new VectorLayerVisualizer("test", mapInformation, null, null); + var modStackSettings = new ModifierStackSettings() { MergeObjects = true }; + var modStack = new ModifierStack(modStackSettings, null); + modStack.MeshModifiers.Add(new LineMeshForPolygonsModifier(new LineMeshParameters() + { + CapType = JoinType.Butt, + JoinType = JoinType.Round, + Width = 6 + } )); + + viz.AddModifierStack(new List() { modStack }); + viz.Initialize(); + return viz; + } + + [Test, Performance, Order(0)] + public void Decompress() + { + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tile = new Mapbox.VectorTile.VectorTile(decompressed); + }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + private static bool[] _doChamfer = new bool[] { true, false}; + + + [UnityTest, Performance, Order(1)] + public IEnumerator BuildingVisualizer([ValueSource("_doChamfer")] bool doChamfer) + { + var settings = doChamfer + ? new BuildingComponentSettings() { RoundBuildingCorners = true } + : new BuildingComponentSettings() { RoundBuildingCorners = false }; + var vizObj = (BuildingComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_buildingVisualizerAssetPath, typeof(BuildingComponentVisualizerObject)); + vizObj.Settings = settings; + var viz = (BuildingComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => { RunBuildingComponent(viz); }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [UnityTest, Performance, Order(3)] + public IEnumerator SingleBuildingVisualizer([ValueSource("_doChamfer")] bool doChamfer) + { + var settings = doChamfer + ? new BuildingComponentSettings() { RoundBuildingCorners = true } + : new BuildingComponentSettings() { RoundBuildingCorners = false }; + var vizObj = (BuildingComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_buildingVisualizerAssetPath, typeof(BuildingComponentVisualizerObject)); + vizObj.Settings = settings; + var viz = (BuildingComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => { RunBuildingComponent(viz); }) + .WarmupCount(0) + .MeasurementCount(5) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + private void RunBuildingComponent(BuildingComponentVisualizer viz) + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorModule.ComponentSystem.Data.VectorTile(decompressed); + viz.ClearCaches(); + if (tt.TryGetLayer("building", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + } + + + [Test, Performance, Order(2)] + public void BuildingVisualizerOldChamfer() + { + var vizObj = (VectorLayerVisualizerObject) AssetDatabase.LoadAssetAtPath(_oldBuildVisAssetPath, typeof(VectorLayerVisualizerObject)); + var viz = (VectorLayerVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + + Measure.Method(() => { RunOldBuildings(viz); }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [Test, Performance, Order(4)] + public void SingleBuildingVisualizerOldChamfer() + { + var vizObj = (VectorLayerVisualizerObject) AssetDatabase.LoadAssetAtPath(_oldBuildVisAssetPath, typeof(VectorLayerVisualizerObject)); + var viz = (VectorLayerVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + + Measure.Method(() => { RunOldBuildings(viz); }) + .WarmupCount(0) + .MeasurementCount(5) + .IterationsPerMeasurement(1) + .GC() + .Run(); + + } + + private void RunOldBuildings(VectorLayerVisualizer viz) + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorTile.VectorTile(decompressed, false); + viz.ClearCaches(); + var layer = tt.GetLayer("building"); + //if (tt.TryGetLayer("building", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + } + } +} \ No newline at end of file diff --git a/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta new file mode 100644 index 000000000..1d148c0b3 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingLayerPerformanceTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 519e05ae014b6402a97f6bd1f2e37455 +timeCreated: 1727879657 \ No newline at end of file diff --git a/Tests/Editor/VectorModule/BuildingSetups.meta b/Tests/Editor/VectorModule/BuildingSetups.meta new file mode 100644 index 000000000..99332a38f --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2fb6ba6d29c104dd1a4010a057753a2e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset new file mode 100644 index 000000000..f8f75a57a --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c6eb41c8ba1c475ab45d048dafafaf77, type: 3} + m_Name: TEST_BuildingLayerVisualizerObject + m_EditorClassIdentifier: + Settings: + EnableTerrainSnapping: 0 + RoundBuildingCorners: 1 + Material: {fileID: 0} + ChamferExtrusionSettings: + OffsetInMeters: 1 + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + BasicExtrusionSettings: + ExtrusionOptions: + extrusionType: 1 + propertyName: height + minimumHeight: 0 + maximumHeight: 1000 + extrusionScaleFactor: 1 + IsActive: 1 diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta new file mode 100644 index 000000000..5a045b76e --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_BuildingLayerVisualizerObject.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46dd07326358e4050b52f8020c1fab1f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset new file mode 100644 index 000000000..c9e6992c2 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dbafb4fa6dff39e41992450d63b53648, type: 3} + m_Name: TEST_OldLayerVisualizer + m_EditorClassIdentifier: + _vectorLayerName: + _settings: + Offset: {x: 0, y: 0, z: 0} + StackExecutionMode: 0 + _modifierStackObjects: + - {fileID: 11400000, guid: a291f041ea7e74b5c8ea54f067605580, type: 2} diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta new file mode 100644 index 000000000..74fada781 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldLayerVisualizer.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: de09110cfc8be47bfa2b6a530a34e70c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset new file mode 100644 index 000000000..e97c9fe71 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset @@ -0,0 +1,67 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a17705f45444b729e601b91361775ed, type: 3} + m_Name: TEST_OldModStack + m_EditorClassIdentifier: + Settings: + VisibilityZoomRange: {x: 1, y: 20} + MergeObjects: 1 + LayerType: 0 + Filters: {fileID: 1368362044068310752} + MeshModifiers: + - {fileID: 6984920302629165297} + - {fileID: 8207249058559122417} + GoModifiers: [] +--- !u!114 &1368362044068310752 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb8a61183121446bbd024747630bf05c, type: 3} + m_Name: FilterStack + m_EditorClassIdentifier: + Filters: [] + Type: 0 +--- !u!114 &6984920302629165297 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cc1f98279c3d447589a86caced7bbc42, type: 3} + m_Name: PolygonMeshModifierObject + m_EditorClassIdentifier: + m_Active: 1 + Height: 0 +--- !u!114 &8207249058559122417 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 70a1f5720dc24e308ea7af12b02b14c4, type: 3} + m_Name: ChamferHeightModifierObject + m_EditorClassIdentifier: + m_Active: 1 + ExtrusionOptions: + OffsetInMeters: 1 + FlatTops: 0 diff --git a/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta new file mode 100644 index 000000000..df3567cd3 --- /dev/null +++ b/Tests/Editor/VectorModule/BuildingSetups/TEST_OldModStack.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a291f041ea7e74b5c8ea54f067605580 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs new file mode 100644 index 000000000..8d29b996c --- /dev/null +++ b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs @@ -0,0 +1,147 @@ +using System.Collections; +using System.Collections.Generic; +using Mapbox.BaseModule.Data.Interfaces; +using Mapbox.BaseModule.Data.Platform; +using Mapbox.BaseModule.Data.Platform.Cache; +using Mapbox.BaseModule.Data.Tiles; +using Mapbox.BaseModule.Map; +using Mapbox.BaseModule.Utilities; +using Mapbox.VectorModule; +using Mapbox.VectorModule.ComponentSystem.BuildingComponentVisualizer; +using Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer; +using Mapbox.VectorModule.MeshGeneration; +using Mapbox.VectorModule.MeshGeneration.MeshModifiers; +using NUnit.Framework; +using Unity.PerformanceTesting; +using UnityEditor; +using UnityEngine.TestTools; + +namespace Mapbox.VectorModuleTests +{ + public class RoadLayerPerformanceTests + { + private int SampleCount = 100; + private ResilientWebRequestFileSource _fs; + private byte[] buffer; + private CanonicalTileId tileId = new CanonicalTileId(15, 18654, 9481); + private string LatLng = "60.1664427,24.9318587"; + private string _roadVisualizerAssetPath = "Packages/com.mapbox.sdk/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset"; + [UnitySetUp] + public IEnumerator SetUp() + { + var mapboxContext = new MapboxContext(); + _fs = new ResilientWebRequestFileSource(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + + Response response = null; + var req = _fs.Request("https://api.mapbox.com/v4/mapbox.mapbox-streets-v7/" + tileId + ".vector.pbf", (r) => + { + response = r; + }); + while (!req.IsCompleted) + { + yield return null; + } + + buffer = response.Data; + Assert.NotZero(buffer.Length); + } + + private IMapInformation GetMapInformation() + { + var mapInformation = new MapInformation(LatLng); + mapInformation.SetInformation(null, 16, 0, 0, 1000); + mapInformation.Initialize(); + return mapInformation; + } + + [UnityTest, Performance] + public IEnumerator RoadVisualizer() + { + //var viz = new RoadComponentVisualizer("test", GetMapInformation(), null, new RoadComponentSettings()); + var vizObj = (RoadComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_roadVisualizerAssetPath, typeof(RoadComponentVisualizerObject)); + var viz = (RoadComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorModule.ComponentSystem.Data.VectorTile(decompressed); + viz.ClearCaches(); + if (tt.TryGetLayer("road", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [UnityTest, Performance] + public IEnumerator SingleRoadVisualizer() + { + //var viz = new RoadComponentVisualizer("test", GetMapInformation(), null, new RoadComponentSettings()); + var vizObj = (RoadComponentVisualizerObject) AssetDatabase.LoadAssetAtPath(_roadVisualizerAssetPath, typeof(RoadComponentVisualizerObject)); + var viz = (RoadComponentVisualizer) vizObj.ConstructLayerVisualizer(GetMapInformation(), null); + yield return viz.Initialize(); + + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorModule.ComponentSystem.Data.VectorTile(decompressed); + viz.ClearCaches(); + if (tt.TryGetLayer("road", out var layer)) + { + viz.CreateMesh(tileId, layer); + } + }) + .WarmupCount(0) + .MeasurementCount(5) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + [Test, Performance] + public void RoadVisualizerOld() + { + var reg = RegularRoadLayer(GetMapInformation()); + Measure.Method(() => + { + var decompressed = Compression.Decompress(buffer); + var tt = new VectorTile.VectorTile(decompressed, false); + reg.ClearCaches(); + var layer = tt.GetLayer("road"); + //if (tt.TryGetLayer("building", out var layer)) + { + reg.CreateMesh(tileId, layer); + } + }) + .WarmupCount(5) + .MeasurementCount(SampleCount) + .IterationsPerMeasurement(1) + .GC() + .Run(); + } + + private VectorLayerVisualizer RegularRoadLayer(IMapInformation mapInformation) + { + var viz = new VectorLayerVisualizer("test", mapInformation, null, null); + var modStackSettings = new ModifierStackSettings() { MergeObjects = true }; + var modStack = new ModifierStack(modStackSettings, null); + modStack.MeshModifiers.Add(new LineMeshForPolygonsModifier(new LineMeshParameters() + { + CapType = JoinType.Butt, + JoinType = JoinType.Round, + Width = 6 + } )); + + viz.AddModifierStack(new List() { modStack }); + viz.Initialize(); + return viz; + } + + } +} \ No newline at end of file diff --git a/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta new file mode 100644 index 000000000..72fa1de93 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadLayerPerformanceTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af9097243e404b70a404c0738447631e +timeCreated: 1765533448 \ No newline at end of file diff --git a/Tests/Editor/VectorModule/RoadSetup.meta b/Tests/Editor/VectorModule/RoadSetup.meta new file mode 100644 index 000000000..380a2a62e --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f2ffe4e624884e7e968f5d00eddee7a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset new file mode 100644 index 000000000..96fd1bb87 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset @@ -0,0 +1,102 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bf8c281932374fb0b5c80fa7fbca2b5b, type: 3} + m_Name: TEST_BasicRoadStyleSheet + m_EditorClassIdentifier: + Styles: + - Name: secondary,tertiary + Filters: + Filters: + - rid: 5722777937161748527 + Type: 0 + Width: 4.8 + PushUp: 0.0008 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: primary + Filters: + Filters: + - rid: 5722777937161748529 + Type: 0 + Width: 5.2 + PushUp: 0.001 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: street,street_limited + Filters: + Filters: + - rid: 5722777937161748530 + Type: 0 + Width: 3.6 + PushUp: 0.0004 + Material: {fileID: 2100000, guid: 8917c0d338e58481388fbe080cf36caf, type: 2} + - Name: service + Filters: + Filters: + - rid: 5722777937161748531 + Type: 0 + Width: 1.8 + PushUp: 0.0002 + Material: {fileID: 2100000, guid: d449f6eb93a644716baa6f1f4c730813, type: 2} + - Name: path + Filters: + Filters: + - rid: 5722777987101753359 + - rid: 5722777987101753361 + - rid: 5722777987101753362 + Type: 0 + Width: 1 + PushUp: 0 + Material: {fileID: 2100000, guid: c3c69f315ce7c4100a0ff6f40305bfe9, type: 2} + references: + version: 2 + RefIds: + - rid: 5722777937161748527 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: secondary,tertiary + Invert: 0 + - rid: 5722777937161748529 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: primary + Invert: 0 + - rid: 5722777937161748530 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 1 + FilterString: street,street_limited + Invert: 0 + - rid: 5722777937161748531 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: service + Invert: 0 + - rid: 5722777987101753359 + type: {class: RoadClassFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: path + Invert: 0 + - rid: 5722777987101753361 + type: {class: RoadStructureFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: tunnel + Invert: 1 + - rid: 5722777987101753362 + type: {class: RoadTypeFilter, ns: Mapbox.VectorModule.ComponentSystem.RoadComponentVisualizer, asm: MapboxComponentSystem} + data: + CheckOperation: 0 + FilterString: steps + Invert: 1 diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta new file mode 100644 index 000000000..3bf74b31f --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoadStyleSheet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 128b299ad7c164ddbb2d2d02f2b18ed7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset new file mode 100644 index 000000000..99c925d51 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e926a31006c7a440ea45ffe7b2bcfff6, type: 3} + m_Name: TEST_BasicRoads + m_EditorClassIdentifier: + Settings: + GameObjectOffset: 0.0001 + RandomOffsetRange: 0.0001 + RoadStyleSheet: {fileID: 11400000, guid: 128b299ad7c164ddbb2d2d02f2b18ed7, type: 2} + IsActive: 1 diff --git a/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta new file mode 100644 index 000000000..ad9cc3477 --- /dev/null +++ b/Tests/Editor/VectorModule/RoadSetup/TEST_BasicRoads.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 076ae6f26b84c457aa3bf8e5ef96b1d6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/VectorModule/VectorModuleTests.asmdef b/Tests/Editor/VectorModule/VectorModuleTests.asmdef new file mode 100644 index 000000000..3f8bc1c3c --- /dev/null +++ b/Tests/Editor/VectorModule/VectorModuleTests.asmdef @@ -0,0 +1,30 @@ +{ + "name": "VectorModuleTests", + "rootNamespace": "", + "references": [ + "GUID:093b9fb2cd4794a8c94472d55c8bb0a9", + "GUID:2400806fb903448e5b7e737b92f1e434", + "GUID:9f843d4b38153f04b9cee0502d568525", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:c0dd0d10738d4ad4a9de57c559d0ca1b", + "GUID:5aef8304858a8459cb2204d15bf75ab9" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Mapbox.VectorTile.VectorTileReader.dll", + "Mapbox.VectorTile.Geometry.dll" + ], + "autoReferenced": true, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta b/Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta new file mode 100644 index 000000000..422002604 --- /dev/null +++ b/Tests/Editor/VectorModule/VectorModuleTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 398284599e92e436e986b419aeda78a3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/BaseModule/ImageSourceTests.cs b/Tests/Runtime/BaseModule/ImageSourceTests.cs index 7e7662bce..2c0765b27 100644 --- a/Tests/Runtime/BaseModule/ImageSourceTests.cs +++ b/Tests/Runtime/BaseModule/ImageSourceTests.cs @@ -30,10 +30,14 @@ internal class ImageSourceTests private CanonicalTileId _testTileId = new CanonicalTileId(16, 5, 7); private Texture2D _testTexture; private RasterData _testRasterData; - - [OneTimeSetUp] - public void OneTimeSetUp() + private bool _initialized; + + [UnitySetUp] + public IEnumerator OneTimeSetUp() { + if (_initialized) yield break; + _initialized = true; + _testTexture = Texture2D.whiteTexture; _testRasterData = new RasterData() { @@ -44,8 +48,9 @@ public void OneTimeSetUp() ExpirationDate = DateTime.Now.AddHours(1), ETag = "testETAG" }; - + var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs b/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs index 9897a5903..81d9dd628 100644 --- a/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs +++ b/Tests/Runtime/BaseModule/LocationGamePrefabTests.cs @@ -64,7 +64,7 @@ void OnMapCoreInitialized(MapboxMap mapboxMap) _mapCore.Initialized += OnMapCoreInitialized; - _mapCore.Initialize(); + yield return _mapCore.Initialize(); while(_mapCore.InitializationStatus < InitializationStatus.ReadyForUpdates) yield return null; diff --git a/Tests/Runtime/BaseModule/MapTest.cs b/Tests/Runtime/BaseModule/MapTest.cs index 6d1b84b38..9d64277d7 100644 --- a/Tests/Runtime/BaseModule/MapTest.cs +++ b/Tests/Runtime/BaseModule/MapTest.cs @@ -49,14 +49,15 @@ public class MapTest : MonoBehaviour // // yield return _map.Initialize(); // } - private void LoadMap(string latlng, bool useSqlite = true, bool useFileCache = true) + private IEnumerator LoadMap(string latlng, bool useSqlite = true, bool useFileCache = true) { var mapInfo = new MapInformation(latlng); mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); - unityContext.Initialize(); + yield return unityContext.Initialize(); var taskManager = unityContext.TaskManager; _dataManager = new LoggingDataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); @@ -118,7 +119,7 @@ private void LoadMap(string latlng, bool useSqlite = true, bool useFileCache = t mapInfo, _vectorSource, unityContext, - new Dictionary(), + new Dictionary>(), new VectorModuleSettings() { DataSettings = vectorSourceSettings }); mapVisualizer.LayerModules.Add(_imageLayer); mapVisualizer.LayerModules.Add(_terrainLayer); @@ -134,7 +135,7 @@ public void TearDown() [UnityTest] public IEnumerator LoadMapView() { - LoadMap(_helsinkiLatitudeLongitudeString); + yield return LoadMap(_helsinkiLatitudeLongitudeString); yield return _map.Initialize(); var tileCover = new TileCover(); @@ -152,8 +153,8 @@ public IEnumerator LoadMapOfExpiredData() { //this wrongly assumes data is already available in the long term cache so it's incomplete - LoadMap(_helsinkiLatitudeLongitudeString); - + yield return LoadMap(_helsinkiLatitudeLongitudeString); + //expire all tiles in db var sqliteCache = _cacheManager.SqLiteCache as SqliteCache; foreach (var tile in sqliteCache.GetAllTiles()) @@ -183,7 +184,7 @@ public IEnumerator LoadMapOfExpiredData() [UnityTest] public IEnumerator LoadMapWithoutSql() { - LoadMap(_helsinkiLatitudeLongitudeString, false); + yield return LoadMap(_helsinkiLatitudeLongitudeString, false); yield return _map.Initialize(); yield return _map.LoadMapViewCoroutine(); @@ -198,7 +199,7 @@ public IEnumerator LoadMapWithoutSql() [UnityTest] public IEnumerator LoadMapWithoutFile() { - LoadMap(_helsinkiLatitudeLongitudeString, true, false); + yield return LoadMap(_helsinkiLatitudeLongitudeString, true, false); yield return _map.Initialize(); yield return _map.LoadMapViewCoroutine(); @@ -214,7 +215,7 @@ public IEnumerator LoadMapWithoutFile() public IEnumerator LoadThenCancelAllRequests() { MapboxCacheManager.DeleteAllCache(); - LoadMap(_helsinkiLatitudeLongitudeString); + yield return LoadMap(_helsinkiLatitudeLongitudeString); yield return _map.Initialize(); _dataManager.SetDelayTime(5); diff --git a/Tests/Runtime/BaseModule/MapViewLoadTests.cs b/Tests/Runtime/BaseModule/MapViewLoadTests.cs index 64c3d8254..cd4ba96e3 100644 --- a/Tests/Runtime/BaseModule/MapViewLoadTests.cs +++ b/Tests/Runtime/BaseModule/MapViewLoadTests.cs @@ -48,8 +48,9 @@ public IEnumerator OneTimeSetUp() mapInfo.SetInformation(null, 16, 45, null, 1000); mapInfo.Initialize(); var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); - unityContext.Initialize(); + yield return unityContext.Initialize(); var taskManager = new TaskManager(); taskManager.Initialize(); @@ -102,7 +103,7 @@ public IEnumerator OneTimeSetUp() mapInfo, _vectorSource, unityContext, - new Dictionary(), + new Dictionary>(), new VectorModuleSettings() { DataSettings = vectorSourceSettings}); mapVisualizer.LayerModules.Add(_imageLayer); mapVisualizer.LayerModules.Add(_terrainLayer); diff --git a/Tests/Runtime/BaseModule/MapboxMapTests.cs b/Tests/Runtime/BaseModule/MapboxMapTests.cs index 95cd90dd5..75f77dc5b 100644 --- a/Tests/Runtime/BaseModule/MapboxMapTests.cs +++ b/Tests/Runtime/BaseModule/MapboxMapTests.cs @@ -9,6 +9,7 @@ using Mapbox.BaseModule.Map; using Mapbox.BaseModule.Unity; using Mapbox.BaseModule.Utilities; +using Mapbox.ImageModule.Terrain; using Mapbox.ImageModule.Terrain.TerrainStrategies; using Mapbox.MapDebug.Scripts.Logging; using Mapbox.UnityMapService; @@ -27,50 +28,59 @@ public class MapboxMapTests private LatitudeLongitude _helsinkiLatLng; private LatitudeLongitude _sfLatLng; private MapboxMap _map; - - [OneTimeSetUp] - public void OneTimeSetUp() + private bool _initialized; + + [UnitySetUp] + public IEnumerator Setup() { - _helsinkiLatLng = Conversions.StringToLatLon(_helsinkiLatitudeLongitudeString); - _sfLatLng = Conversions.StringToLatLon(_sanFranciscoLatitudeLongitudeString); + if (!_initialized) + { + _initialized = true; - var mapInfo = new MapInformation(_helsinkiLatitudeLongitudeString); - mapInfo.SetInformation(null, 16, 45, null, 1000); - mapInfo.Initialize(); - var mapboxContext = new MapboxContext(); - var unityContext = new UnityContext(); - unityContext.Initialize(); + _helsinkiLatLng = Conversions.StringToLatLon(_helsinkiLatitudeLongitudeString); + _sfLatLng = Conversions.StringToLatLon(_sanFranciscoLatitudeLongitudeString); - var taskManager = new TaskManager(); - taskManager.Initialize(); - unityContext.TaskManager = taskManager; - var dataManager = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + var mapInfo = new MapInformation(_helsinkiLatitudeLongitudeString); + mapInfo.SetInformation(null, 16, 45, null, 1000); + mapInfo.Initialize(); + var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); + var unityContext = new UnityContext(); + unityContext.Initialize(); - var sqliteCache = new MockSqliteCache(taskManager); - sqliteCache.ReadySqliteDatabase(); - - var mapService = new MapUnityService( - unityContext, - mapboxContext, - new UnityFixedAreaTileProvider(), - new MapboxCacheManager( - unityContext, - new MemoryCache(), - new MockFileCache(taskManager), - sqliteCache), - dataManager); - - _map = new MapboxMap(mapInfo, unityContext, mapService); - var mapVisualizer = new MapboxMapVisualizer(mapInfo, unityContext, new TileCreator(unityContext)); - _map.MapVisualizer = mapVisualizer; - } + var taskManager = new TaskManager(); + taskManager.Initialize(); + unityContext.TaskManager = taskManager; + var dataManager = new DataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); + + var sqliteCache = new MockSqliteCache(taskManager); + sqliteCache.ReadySqliteDatabase(); + + var mapService = new MapUnityService( + unityContext, + mapboxContext, + new UnityFixedAreaTileProvider(), + new MapboxCacheManager( + unityContext, + new MemoryCache(), + new MockFileCache(taskManager), + sqliteCache), + dataManager); + + _map = new MapboxMap(mapInfo, unityContext, mapService); + var mapVisualizer = new MapboxMapVisualizer(mapInfo, unityContext, new TileCreator(unityContext)); + mapVisualizer.LayerModules.Add( + new TerrainLayerModule(mapService.GetTerrainRasterSource( + new ImageSourceSettings() + { + TilesetId = MapboxDefaultElevation.GetParameters(ElevationSourceType.MapboxTerrain).Id + }), new TerrainLayerModuleSettings())); + _map.MapVisualizer = mapVisualizer; + } - [UnitySetUp] - public IEnumerator Setup() - { var initialization = Runnable.Instance.StartCoroutine(_map.Initialize()); yield return initialization; - + Assert.IsNotNull(_map); Assert.IsNotNull(_map.MapVisualizer); Assert.IsTrue(_map.Status >= InitializationStatus.Initialized); @@ -148,6 +158,47 @@ public void ChangeViewToSF() Assert.AreEqual(_map.MapInformation.Pitch, 45); Assert.AreEqual(_map.MapInformation.Bearing, 30); } + + + private static IEnumerable LatLngElevationsource + { + get + { + yield return new TestCaseData(new LatitudeLongitude(27.9878, 86.9250), 8848f).Returns(null); // Mount Everest summit, Nepal/China + yield return new TestCaseData(new LatitudeLongitude(35.3606, 138.7274), 3776f).Returns(null); // Mount Fuji, Japan + yield return new TestCaseData(new LatitudeLongitude(36.5786, -118.2923), 4421f).Returns(null); // Mount Whitney, USA + yield return new TestCaseData(new LatitudeLongitude(51.1789, -1.8262), 102f).Returns(null); // Stonehenge, UK + + yield return new TestCaseData(new LatitudeLongitude(46.8523, -121.7603), 4372f).Returns(null); // Mount Rainier, USA + yield return new TestCaseData(new LatitudeLongitude(-13.1631, -72.5450), 2430f).Returns(null); // Machu Picchu, Peru + yield return new TestCaseData(new LatitudeLongitude(19.8207, -155.4681), 4207f).Returns(null); // Mauna Kea, USA + + yield return new TestCaseData(new LatitudeLongitude(-25.3444, 131.0369), 863f).Returns(null); // Uluru, Australia + yield return new TestCaseData(new LatitudeLongitude(43.6532, -79.3832), 92f).Returns(null); // Toronto, Canada + yield return new TestCaseData(new LatitudeLongitude(55.7558, 37.6176), 156f).Returns(null); // Moscow, Russia + yield return new TestCaseData(new LatitudeLongitude(35.6895, 139.6917), 38f).Returns(null); // Tokyo, Japan + yield return new TestCaseData(new LatitudeLongitude(-34.6037, -58.3816), 25f).Returns(null); // Buenos Aires, Argentina + + yield return new TestCaseData(new LatitudeLongitude(51.5074, -0.1278), 18f).Returns(null); // London, UK + yield return new TestCaseData(new LatitudeLongitude(37.7749, -122.4194), 16f).Returns(null); // San Francisco, USA + yield return new TestCaseData(new LatitudeLongitude(28.6139, 77.2090), 216f).Returns(null); // New Delhi, India + yield return new TestCaseData(new LatitudeLongitude(41.9028, 12.4964), 52f).Returns(null); // Rome, Italy + yield return new TestCaseData(new LatitudeLongitude(19.4326, -99.1332), 2240f).Returns(null); // Mexico City, Mexico + } + } + + [UnityTest] + [TestCaseSource(nameof(LatLngElevationsource))] + public IEnumerator TestElevations(LatitudeLongitude latLng, float expectedElevation) + { + if (_map.MapVisualizer.TryGetLayerModule(out var terrainModule)) + { + yield return terrainModule.GetElevationData(latLng, (elevation) => + { + Assert.AreEqual(elevation, expectedElevation, expectedElevation * 0.1f, $"{latLng} : {elevation} - {expectedElevation}"); + }); + } + } } public class DataFetcherTests @@ -155,12 +206,16 @@ public class DataFetcherTests private LoggingDataFetchingManager _datafetcher; private CanonicalTileId _tileId; private string _tilesetId; - - - [OneTimeSetUp] - public void OneTimeSetup() + private bool _initialized; + + [UnitySetUp] + public IEnumerator OneTimeSetup() { + if (_initialized) yield break; + _initialized = true; + var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); _datafetcher = new LoggingDataFetchingManager(mapboxContext.GetAccessToken(), mapboxContext.GetSkuToken); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); _tilesetId = vectorTileset.Id; diff --git a/Tests/Runtime/BaseModule/PbfSourceTests.cs b/Tests/Runtime/BaseModule/PbfSourceTests.cs index 908bd89fc..843ac0453 100644 --- a/Tests/Runtime/BaseModule/PbfSourceTests.cs +++ b/Tests/Runtime/BaseModule/PbfSourceTests.cs @@ -26,10 +26,14 @@ internal class PbfSourceTests private CanonicalTileId _testTileId = new CanonicalTileId(16, 5, 7); private Texture2D _testTexture; private VectorData _testVectorData; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetUp() + [UnitySetUp] + public IEnumerator OneTimeSetUp() { + if (_initialized) yield break; + _initialized = true; + _testTexture = Texture2D.whiteTexture; _testVectorData = new VectorData() { @@ -41,6 +45,7 @@ public void OneTimeSetUp() }; var mapboxContext = new MapboxContext(); + yield return mapboxContext.LoadConfigurationCoroutine(false); var unityContext = new UnityContext(); var taskManager = new MockTaskManager(); taskManager.Initialize(); diff --git a/Tests/Runtime/BaseModule/PlayModeTests.asmdef b/Tests/Runtime/BaseModule/PlayModeTests.asmdef index 0a4710e66..8d3610a62 100644 --- a/Tests/Runtime/BaseModule/PlayModeTests.asmdef +++ b/Tests/Runtime/BaseModule/PlayModeTests.asmdef @@ -13,7 +13,9 @@ "MapboxImageryModule", "MapboxVectorModule" ], - "includePlatforms": [], + "includePlatforms": [ + "Editor" + ], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": true, diff --git a/Tests/Runtime/BaseModule/RasterDataTests.cs b/Tests/Runtime/BaseModule/RasterDataTests.cs index e1f3dfe67..a77a85193 100644 --- a/Tests/Runtime/BaseModule/RasterDataTests.cs +++ b/Tests/Runtime/BaseModule/RasterDataTests.cs @@ -31,12 +31,17 @@ internal class RasterDataTests private Style _imageryTileset; private UnwrappedTileId _tileId; private HashSet _testTileHashset; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetup() + [UnitySetUp] + public IEnumerator OneTimeSetup() { + if (_initialized) yield break; + _initialized = true; + _taskManager = new MockTaskManager(); _mapboxContext = new MapboxContext(); + yield return _mapboxContext.LoadConfigurationCoroutine(false); _dataFetchingManager = new DataFetchingManager(_mapboxContext.GetAccessToken(), _mapboxContext.GetSkuToken); _unityContext = new UnityContext(); _unityContext.Initialize(_taskManager); @@ -68,11 +73,11 @@ public void SetUp() [UnityTest, Order(5)] public IEnumerator RequestTileListNoCache() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, null)); - + var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); - + Runnable.EnableRunnableInEditor(); List loadedTiles = null; var coroutine = Runnable.Instance.StartCoroutine(rasterSource.LoadTilesCoroutine(_testTileHashset, (data) => @@ -80,15 +85,15 @@ public IEnumerator RequestTileListNoCache() loadedTiles = data; })); yield return coroutine; - + Assert.NotNull(loadedTiles); Assert.IsNotEmpty(loadedTiles); } - + [UnityTest, Order(4)] public IEnumerator RequestTileListCheckWithFileSqlite() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -122,11 +127,11 @@ public IEnumerator RequestTileListCheckWithFileSqlite() [UnityTest, Order(3)] public IEnumerator RequestWithAllCaches() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); - + var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); - + RasterData resultData = null; var coroutineId = Runnable.Run(rasterSource.LoadTileCoroutine(_tileId.Canonical, (data) => { @@ -137,21 +142,21 @@ public IEnumerator RequestWithAllCaches() { yield return null; } - + Assert.NotNull(resultData.Texture); Assert.AreEqual(resultData.CacheType, CacheType.NoCache); var fileCount = _fileCache.GetFileList().Count; Assert.AreEqual(1, fileCount, "File Cache should have one tile, instead it has " + fileCount); - + //adding same tile as second test so sqlite tile count should still be one var sqliteTileCount = _sqliteCache.GetAllTiles().Count; Assert.AreEqual(1, sqliteTileCount, "Sqlite should have one tile, instead it has " + sqliteTileCount); } - + [UnityTest, Order(2)] public IEnumerator RequestNoFileYesSqliteCache() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, _sqliteCache)); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); @@ -176,7 +181,7 @@ public IEnumerator RequestNoFileYesSqliteCache() public IEnumerator RequestNoFileNoSqliteCaches() { var cacheManager = new MapboxCacheManager(_unityContext, new MemoryCache(), null, null); - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, cacheManager); + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, cacheManager); var rasterSource = mapService.GetStaticRasterSource(new ImageSourceSettings() { TilesetId = _imageryTileset.Id }); diff --git a/Tests/Runtime/BaseModule/SceneTests.cs b/Tests/Runtime/BaseModule/SceneTests.cs index 7dfbc86c9..7a8c71e89 100644 --- a/Tests/Runtime/BaseModule/SceneTests.cs +++ b/Tests/Runtime/BaseModule/SceneTests.cs @@ -187,7 +187,7 @@ public IEnumerator ZoomInTest([ValueSource("fileCache")] bool fileCache, [ValueS { mapCore.Initialized += map => { _map = map; }; mapCore.MapInformation.SetInformation(null, 8); - mapCore.Initialize(); + yield return mapCore.Initialize(); } else { diff --git a/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity b/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity index 6c3820ede..45873c231 100644 --- a/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity +++ b/Tests/Runtime/BaseModule/Scenes/WorldMapScene.unity @@ -583,7 +583,8 @@ MonoBehaviour: MapRoot: {fileID: 1316629400} BaseTileRoot: {fileID: 0} RuntimeGenerationRoot: {fileID: 0} - _tileCreatorBehaviour: {fileID: 1316629406} + _tileCreatorBehaviour: {fileID: 0} + _defaultTileMaterial: {fileID: 2100000, guid: a6a681001a245ce4089aea4634eee2b8, type: 2} TileProvider: {fileID: 0} DataFetcher: {fileID: 0} CacheManager: {fileID: 2143662170} diff --git a/Tests/Runtime/BaseModule/VectorDataTests.cs b/Tests/Runtime/BaseModule/VectorDataTests.cs index 977bb4126..540b5205a 100644 --- a/Tests/Runtime/BaseModule/VectorDataTests.cs +++ b/Tests/Runtime/BaseModule/VectorDataTests.cs @@ -29,12 +29,17 @@ internal class VectorDataTests private ISqliteCache _sqliteCache; private UnwrappedTileId _tileId; private HashSet _testTileHashset; + private bool _initialized; - [OneTimeSetUp] - public void OneTimeSetup() + [UnitySetUp] + public IEnumerator OneTimeSetup() { + if (_initialized) yield break; + _initialized = true; + _taskManager = new MockTaskManager(); _mapboxContext = new MapboxContext(); + yield return _mapboxContext.LoadConfigurationCoroutine(false); _dataFetchingManager = new DataFetchingManager(_mapboxContext.GetAccessToken(), _mapboxContext.GetSkuToken); _unityContext = new UnityContext(); _unityContext.Initialize(_taskManager); @@ -66,12 +71,12 @@ public void SetUp() [UnityTest, Order(4)] public IEnumerator RequestTileList() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); - + var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); - + List loadedTiles = null; var coroutine = Runnable.Instance.StartCoroutine(vectorSource.LoadTilesCoroutine(_testTileHashset, (data) => { @@ -92,11 +97,11 @@ public IEnumerator RequestTileList() [UnityTest, Order(3)] public IEnumerator RequestWithAllCaches() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), _fileCache, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); - + VectorData vectorData = null; var coroutineId = Runnable.Instance.StartCoroutine(vectorSource.LoadTileCoroutine(_tileId.Canonical, (data) => { @@ -112,7 +117,7 @@ public IEnumerator RequestWithAllCaches() [UnityTest, Order(2)] public IEnumerator RequestNoFileYesSqliteCache() { - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, new MapboxCacheManager(_unityContext, new MemoryCache(), null, _sqliteCache)); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); @@ -137,7 +142,7 @@ public IEnumerator RequestNoFileYesSqliteCache() public IEnumerator RequestNoFileNoSqliteCaches() { var cacheManager = new MapboxCacheManager(_unityContext, new MemoryCache(), null, null); - var mapService = new MapUnityService(_unityContext, new MapboxContext(), null, cacheManager); + var mapService = new MapUnityService(_unityContext, _mapboxContext, null, cacheManager); var vectorTileset = MapboxDefaultVector.GetParameters(VectorSourceType.MapboxStreetsV8); var vectorSource = mapService.GetVectorSource(new VectorSourceSettings(){ TilesetId = vectorTileset.Id}); diff --git a/package.json b/package.json index 50815b914..42de87f75 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "com.mapbox.sdk", - "version": "3.0.7", + "version": "3.1.1", "displayName": "Mapbox Unity SDK", "description": "Mapbox Unity SDK", "unity": "2022.3", "dependencies": { "com.unity.render-pipelines.universal": "10.10.1", - "com.unity.nuget.newtonsoft-json": "3.2.1" + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.burst": "1.8.12", + "com.unity.inputsystem": "1.7.0" }, "author": { "name": "Mapbox", @@ -33,6 +35,11 @@ "displayName": "GeocodingApiDemo", "description": "Geocoding Api Demo", "path": "Runtime/Mapbox/GeocodingApi/Samples~/GeocodingApiDemo" + }, + { + "displayName": "MapboxComponents", + "description": "Mapbox Components Demo", + "path": "Samples~/MapboxComponents" } ] }