From a707261e7291256f450b0113dab1431fad57a6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Sun, 15 Feb 2026 15:06:16 +0100 Subject: [PATCH 1/2] feat: add HillshadeLayer component Add full HillshadeLayer support with iOS (Swift + Fabric) and Android native implementations. Uses raster-dem source for terrain visualization. --- __tests__/interface.test.js | 1 + .../java/com/rnmapbox/rnmbx/RNMBXPackage.kt | 2 + .../styles/layers/RNMBXHillshadeLayer.kt | 27 ++ .../layers/RNMBXHillshadeLayerManager.kt | 84 ++++ docs/HillshadeLayer.md | 383 ++++++++++++++++++ docs/docs.json | 273 +++++++++++++ ios/RNMBX/RNMBXHillshadeLayer.swift | 96 +++++ ios/RNMBX/RNMBXHillshadeLayerComponentView.h | 14 + ios/RNMBX/RNMBXHillshadeLayerComponentView.mm | 73 ++++ package.json | 4 + src/Mapbox.native.ts | 1 + src/components/HillshadeLayer.tsx | 108 +++++ .../RNMBXHillshadeLayerNativeComponent.ts | 39 ++ 13 files changed, 1105 insertions(+) create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayer.kt create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayerManager.kt create mode 100644 docs/HillshadeLayer.md create mode 100644 ios/RNMBX/RNMBXHillshadeLayer.swift create mode 100644 ios/RNMBX/RNMBXHillshadeLayerComponentView.h create mode 100644 ios/RNMBX/RNMBXHillshadeLayerComponentView.mm create mode 100644 src/components/HillshadeLayer.tsx create mode 100644 src/specs/RNMBXHillshadeLayerNativeComponent.ts diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js index 693f5d9ee..458d4ed91 100644 --- a/__tests__/interface.test.js +++ b/__tests__/interface.test.js @@ -40,6 +40,7 @@ describe('Public Interface', () => { 'BackgroundLayer', 'RasterLayer', 'RasterParticleLayer', + 'HillshadeLayer', 'SkyLayer', 'Terrain', 'Atmosphere', diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index 57c8cc6cf..b86218ce8 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -31,6 +31,7 @@ import com.rnmapbox.rnmbx.components.styles.layers.RNMBXCircleLayerManager import com.rnmapbox.rnmbx.components.styles.layers.RNMBXFillExtrusionLayerManager import com.rnmapbox.rnmbx.components.styles.layers.RNMBXFillLayerManager import com.rnmapbox.rnmbx.components.styles.layers.RNMBXHeatmapLayerManager +import com.rnmapbox.rnmbx.components.styles.layers.RNMBXHillshadeLayerManager import com.rnmapbox.rnmbx.components.styles.layers.RNMBXLineLayerManager import com.rnmapbox.rnmbx.components.styles.layers.RNMBXModelLayerManager import com.rnmapbox.rnmbx.components.styles.layers.RNMBXRasterLayerManager @@ -161,6 +162,7 @@ class RNMBXPackage : TurboReactPackage() { managers.add(RNMBXCircleLayerManager()) managers.add(RNMBXSymbolLayerManager()) managers.add(RNMBXRasterLayerManager()) + managers.add(RNMBXHillshadeLayerManager()) if (RNMBXRasterParticleLayerManager.isImplemented) { managers.add(RNMBXRasterParticleLayerManager()) } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayer.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayer.kt new file mode 100644 index 000000000..7771e7993 --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayer.kt @@ -0,0 +1,27 @@ +package com.rnmapbox.rnmbx.components.styles.layers + +import android.content.Context +import com.mapbox.maps.extension.style.layers.generated.HillshadeLayer +import com.rnmapbox.rnmbx.components.styles.RNMBXStyle +import com.rnmapbox.rnmbx.components.styles.RNMBXStyleFactory +import com.rnmapbox.rnmbx.utils.Logger + +class RNMBXHillshadeLayer(context: Context?) : RNMBXLayer( + context!! +) { + override fun makeLayer(): HillshadeLayer { + return HillshadeLayer(iD!!, mSourceID!!) + } + + override fun addStyles() { + mLayer?.also { + RNMBXStyleFactory.setHillshadeLayerStyle(it, RNMBXStyle(context, mReactStyle, mMap!!)) + } ?: run { + Logger.e("RNMBXHillshadeLayer", "mLayer is null") + } + } + + fun setSourceLayerID(asString: String?) { + // no-op + } +} diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayerManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayerManager.kt new file mode 100644 index 000000000..fe6fcd1c2 --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/styles/layers/RNMBXHillshadeLayerManager.kt @@ -0,0 +1,84 @@ +package com.rnmapbox.rnmbx.components.styles.layers + +import com.facebook.react.bridge.Dynamic +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.RNMBXHillshadeLayerManagerInterface + +class RNMBXHillshadeLayerManager : ViewGroupManager(), + RNMBXHillshadeLayerManagerInterface { + override fun getName(): String { + return REACT_CLASS + } + + override fun createViewInstance(reactContext: ThemedReactContext): RNMBXHillshadeLayer { + return RNMBXHillshadeLayer(reactContext) + } + + // @{codepart-replace-start(LayerManagerCommonProps.codepart-kt.ejs,{layerType:"RNMBXHillshadeLayer"})} + @ReactProp(name = "id") + override fun setId(layer: RNMBXHillshadeLayer, id: Dynamic) { + layer.iD = id.asString() + } + + @ReactProp(name = "existing") + override fun setExisting(layer: RNMBXHillshadeLayer, existing: Dynamic) { + layer.setExisting(existing.asBoolean()) + } + + @ReactProp(name = "sourceID") + override fun setSourceID(layer: RNMBXHillshadeLayer, sourceID: Dynamic) { + layer.setSourceID(sourceID.asString()) + } + + @ReactProp(name = "aboveLayerID") + override fun setAboveLayerID(layer: RNMBXHillshadeLayer, aboveLayerID: Dynamic) { + layer.setAboveLayerID(aboveLayerID.asString()) + } + + @ReactProp(name = "belowLayerID") + override fun setBelowLayerID(layer: RNMBXHillshadeLayer, belowLayerID: Dynamic) { + layer.setBelowLayerID(belowLayerID.asString()) + } + + @ReactProp(name = "layerIndex") + override fun setLayerIndex(layer: RNMBXHillshadeLayer, layerIndex: Dynamic) { + layer.setLayerIndex(layerIndex.asInt()) + } + + @ReactProp(name = "minZoomLevel") + override fun setMinZoomLevel(layer: RNMBXHillshadeLayer, minZoomLevel: Dynamic) { + layer.setMinZoomLevel(minZoomLevel.asDouble()) + } + + @ReactProp(name = "maxZoomLevel") + override fun setMaxZoomLevel(layer: RNMBXHillshadeLayer, maxZoomLevel: Dynamic) { + layer.setMaxZoomLevel(maxZoomLevel.asDouble()) + } + + @ReactProp(name = "reactStyle") + override fun setReactStyle(layer: RNMBXHillshadeLayer, style: Dynamic) { + layer.setReactStyle(style.asMap()) + } + + @ReactProp(name = "sourceLayerID") + override fun setSourceLayerID(layer: RNMBXHillshadeLayer, sourceLayerID: Dynamic) { + layer.setSourceLayerID(sourceLayerID.asString()) + } + + @ReactProp(name = "filter") + override fun setFilter(layer: RNMBXHillshadeLayer, filterList: Dynamic) { + layer.setFilter(filterList.asArray()) + } + + @ReactProp(name = "slot") + override fun setSlot(layer: RNMBXHillshadeLayer, slot: Dynamic) { + layer.setSlot(slot.asString()) + } + // @{codepart-replace-end} + + companion object { + const val REACT_CLASS = "RNMBXHillshadeLayer" + } +} diff --git a/docs/HillshadeLayer.md b/docs/HillshadeLayer.md new file mode 100644 index 000000000..2ca0a0e1f --- /dev/null +++ b/docs/HillshadeLayer.md @@ -0,0 +1,383 @@ + + + Mapbox spec: [hillshade](https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#hillshade) + + +```tsx +import { HillshadeLayer } from '@rnmapbox/maps'; + +HillshadeLayer + +``` + + +## props + + +### id + +```tsx +string +``` +_required_ +A string that uniquely identifies the source in the style to which it is added. + + + +### existing + +```tsx +boolean +``` +The id refers to an existing layer in the style. Does not create a new layer. + + + +### sourceID + +```tsx +string +``` +The source from which to obtain the data to style. +If the source has not yet been added to the current style, the behavior is undefined. +Inferred from parent source only if the layer is a direct child to it. + + _defaults to:_ `Mapbox.StyleSource.DefaultSourceID` + + +### sourceLayerID + +```tsx +string +``` +Identifier of the layer within the source identified by the sourceID property from which the receiver obtains the data to style. + + + +### aboveLayerID + +```tsx +string +``` +Inserts a layer above aboveLayerID. + + + +### belowLayerID + +```tsx +string +``` +Inserts a layer below belowLayerID + + + +### layerIndex + +```tsx +number +``` +Inserts a layer at a specified index + + + +### filter + +```tsx +FilterExpression +``` +Filter only the features in the source layer that satisfy a condition that you define + + + +### minZoomLevel + +```tsx +number +``` +The minimum zoom level at which the layer gets parsed and appears. + + + +### maxZoomLevel + +```tsx +number +``` +The maximum zoom level at which the layer gets parsed and appears. + + + +### slot + +```tsx +'bottom' | 'middle' | 'top' +``` +The slot this layer is assigned to. If specified, and a slot with that name exists, it will be placed at that position in the layer order. + +v11 only + + + +### style + +```tsx +HillshadeLayerStyleProps +``` +_required_ +Customizable style attributes + + + + + + + + + +## styles + +* visibility
+* hillshadeIlluminationDirection
+* hillshadeIlluminationAnchor
+* hillshadeExaggeration
+* hillshadeShadowColor
+* hillshadeHighlightColor
+* hillshadeAccentColor
+ +___ + +### visibility +Name: `visibility` + +Mapbox spec: [visibility](https://docs.mapbox.com/style-spec/reference/layers/#layout-hillshade-visibility) + +#### Description +Whether this layer is displayed. + +#### Type +`enum` +#### Default Value +`visible` + +#### Supported Values +**visible** - The layer is shown.
+**none** - The layer is not shown.
+ + +#### Expression + +Parameters: `` + +___ + +### hillshadeIlluminationDirection +Name: `hillshadeIlluminationDirection` + +Mapbox spec: [hillshade-illumination-direction](https://docs.mapbox.com/style-spec/reference/layers/#paint-hillshade-hillshade-illumination-direction) + +#### Description +The direction of the light source used to generate the hillshading with 0 as the top of the viewport if `hillshadeIlluminationAnchor` is set to `viewport` and due north if `hillshadeIlluminationAnchor` is set to `map` and no 3d lights enabled. If `hillshadeIlluminationAnchor` is set to `map` and 3d lights enabled, the direction from 3d lights is used instead. + +#### Type +`number` +#### Default Value +`335` + +#### Minimum +`0` + + +#### Maximum +`359` + +#### Expression + +Parameters: `zoom` + +___ + +### hillshadeIlluminationAnchor +Name: `hillshadeIlluminationAnchor` + +Mapbox spec: [hillshade-illumination-anchor](https://docs.mapbox.com/style-spec/reference/layers/#paint-hillshade-hillshade-illumination-anchor) + +#### Description +Direction of light source when map is rotated. + +#### Type +`enum` +#### Default Value +`viewport` + +#### Supported Values +**map** - The hillshade illumination is relative to the north direction.
+**viewport** - The hillshade illumination is relative to the top of the viewport.
+ + +#### Expression + +Parameters: `zoom` + +___ + +### hillshadeExaggeration +Name: `hillshadeExaggeration` + +Mapbox spec: [hillshade-exaggeration](https://docs.mapbox.com/style-spec/reference/layers/#paint-hillshade-hillshade-exaggeration) + +#### Description +Intensity of the hillshade + +#### Type +`number` +#### Default Value +`0.5` + +#### Minimum +`0` + + +#### Maximum +`1` + +#### Expression + +Parameters: `zoom` +___ + +### hillshadeExaggerationTransition +Name: `hillshadeExaggerationTransition` + +#### Description + +The transition affecting any changes to this layer’s hillshadeExaggeration property. + +#### Type + +`{ duration, delay }` + +#### Units +`milliseconds` + +#### Default Value +`{duration: 300, delay: 0}` + + +___ + +### hillshadeShadowColor +Name: `hillshadeShadowColor` + +Mapbox spec: [hillshade-shadow-color](https://docs.mapbox.com/style-spec/reference/layers/#paint-hillshade-hillshade-shadow-color) + +#### Description +The shading color of areas that face away from the light source. + +#### Type +`color` +#### Default Value +`#000000` + + +#### Expression + +Parameters: `zoom, measure-light` +___ + +### hillshadeShadowColorTransition +Name: `hillshadeShadowColorTransition` + +#### Description + +The transition affecting any changes to this layer’s hillshadeShadowColor property. + +#### Type + +`{ duration, delay }` + +#### Units +`milliseconds` + +#### Default Value +`{duration: 300, delay: 0}` + + +___ + +### hillshadeHighlightColor +Name: `hillshadeHighlightColor` + +Mapbox spec: [hillshade-highlight-color](https://docs.mapbox.com/style-spec/reference/layers/#paint-hillshade-hillshade-highlight-color) + +#### Description +The shading color of areas that faces towards the light source. + +#### Type +`color` +#### Default Value +`#FFFFFF` + + +#### Expression + +Parameters: `zoom, measure-light` +___ + +### hillshadeHighlightColorTransition +Name: `hillshadeHighlightColorTransition` + +#### Description + +The transition affecting any changes to this layer’s hillshadeHighlightColor property. + +#### Type + +`{ duration, delay }` + +#### Units +`milliseconds` + +#### Default Value +`{duration: 300, delay: 0}` + + +___ + +### hillshadeAccentColor +Name: `hillshadeAccentColor` + +Mapbox spec: [hillshade-accent-color](https://docs.mapbox.com/style-spec/reference/layers/#paint-hillshade-hillshade-accent-color) + +#### Description +The shading color used to accentuate rugged terrain like sharp cliffs and gorges. + +#### Type +`color` +#### Default Value +`#000000` + + +#### Expression + +Parameters: `zoom, measure-light` +___ + +### hillshadeAccentColorTransition +Name: `hillshadeAccentColorTransition` + +#### Description + +The transition affecting any changes to this layer’s hillshadeAccentColor property. + +#### Type + +`{ duration, delay }` + +#### Units +`milliseconds` + +#### Default Value +`{duration: 300, delay: 0}` + + diff --git a/docs/docs.json b/docs/docs.json index 6d6bd98b2..56060783a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3038,6 +3038,279 @@ } ] }, + "HillshadeLayer": { + "description": "", + "displayName": "HillshadeLayer", + "methods": [], + "props": [ + { + "name": "id", + "required": true, + "type": "string", + "default": "none", + "description": "A string that uniquely identifies the source in the style to which it is added." + }, + { + "name": "existing", + "required": false, + "type": "boolean", + "default": "none", + "description": "The id refers to an existing layer in the style. Does not create a new layer." + }, + { + "name": "sourceID", + "required": false, + "type": "string", + "default": "Mapbox.StyleSource.DefaultSourceID", + "description": "The source from which to obtain the data to style.\nIf the source has not yet been added to the current style, the behavior is undefined.\nInferred from parent source only if the layer is a direct child to it." + }, + { + "name": "sourceLayerID", + "required": false, + "type": "string", + "default": "none", + "description": "Identifier of the layer within the source identified by the sourceID property from which the receiver obtains the data to style." + }, + { + "name": "aboveLayerID", + "required": false, + "type": "string", + "default": "none", + "description": "Inserts a layer above aboveLayerID." + }, + { + "name": "belowLayerID", + "required": false, + "type": "string", + "default": "none", + "description": "Inserts a layer below belowLayerID" + }, + { + "name": "layerIndex", + "required": false, + "type": "number", + "default": "none", + "description": "Inserts a layer at a specified index" + }, + { + "name": "filter", + "required": false, + "type": "FilterExpression", + "default": "none", + "description": "Filter only the features in the source layer that satisfy a condition that you define" + }, + { + "name": "minZoomLevel", + "required": false, + "type": "number", + "default": "none", + "description": "The minimum zoom level at which the layer gets parsed and appears." + }, + { + "name": "maxZoomLevel", + "required": false, + "type": "number", + "default": "none", + "description": "The maximum zoom level at which the layer gets parsed and appears." + }, + { + "name": "slot", + "required": false, + "type": "'bottom' \\| 'middle' \\| 'top'", + "default": "none", + "description": "The slot this layer is assigned to. If specified, and a slot with that name exists, it will be placed at that position in the layer order.\n\nv11 only" + }, + { + "name": "style", + "required": true, + "type": "HillshadeLayerStyleProps", + "default": "none", + "description": "Customizable style attributes" + } + ], + "fileNameWithExt": "HillshadeLayer.tsx", + "relPath": "src/components/HillshadeLayer.tsx", + "name": "HillshadeLayer", + "mbx": { + "name": "hillshade" + }, + "styles": [ + { + "name": "visibility", + "type": "enum", + "values": [ + { + "value": "visible", + "doc": "The layer is shown." + }, + { + "value": "none", + "doc": "The layer is not shown." + } + ], + "default": "visible", + "description": "Whether this layer is displayed.", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": false + }, + "mbx": { + "fullName": "layout-hillshade-visibility", + "name": "visibility", + "namespace": "layout" + } + }, + { + "name": "hillshadeIlluminationDirection", + "type": "number", + "values": [], + "minimum": 0, + "maximum": 359, + "default": 335, + "description": "The direction of the light source used to generate the hillshading with 0 as the top of the viewport if `hillshadeIlluminationAnchor` is set to `viewport` and due north if `hillshadeIlluminationAnchor` is set to `map` and no 3d lights enabled. If `hillshadeIlluminationAnchor` is set to `map` and 3d lights enabled, the direction from 3d lights is used instead.", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": false, + "mbx": { + "fullName": "paint-hillshade-hillshade-illumination-direction", + "name": "hillshade-illumination-direction", + "namespace": "paint" + } + }, + { + "name": "hillshadeIlluminationAnchor", + "type": "enum", + "values": [ + { + "value": "map", + "doc": "The hillshade illumination is relative to the north direction." + }, + { + "value": "viewport", + "doc": "The hillshade illumination is relative to the top of the viewport." + } + ], + "default": "viewport", + "description": "Direction of light source when map is rotated.", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": false, + "parameters": [ + "zoom" + ] + }, + "mbx": { + "fullName": "paint-hillshade-hillshade-illumination-anchor", + "name": "hillshade-illumination-anchor", + "namespace": "paint" + } + }, + { + "name": "hillshadeExaggeration", + "type": "number", + "values": [], + "minimum": 0, + "maximum": 1, + "default": 0.5, + "description": "Intensity of the hillshade", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "mbx": { + "fullName": "paint-hillshade-hillshade-exaggeration", + "name": "hillshade-exaggeration", + "namespace": "paint" + } + }, + { + "name": "hillshadeShadowColor", + "type": "color", + "values": [], + "default": "#000000", + "description": "The shading color of areas that face away from the light source.", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": true, + "parameters": [ + "zoom", + "measure-light" + ] + }, + "transition": true, + "mbx": { + "fullName": "paint-hillshade-hillshade-shadow-color", + "name": "hillshade-shadow-color", + "namespace": "paint" + } + }, + { + "name": "hillshadeHighlightColor", + "type": "color", + "values": [], + "default": "#FFFFFF", + "description": "The shading color of areas that faces towards the light source.", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": true, + "parameters": [ + "zoom", + "measure-light" + ] + }, + "transition": true, + "mbx": { + "fullName": "paint-hillshade-hillshade-highlight-color", + "name": "hillshade-highlight-color", + "namespace": "paint" + } + }, + { + "name": "hillshadeAccentColor", + "type": "color", + "values": [], + "default": "#000000", + "description": "The shading color used to accentuate rugged terrain like sharp cliffs and gorges.", + "requires": [], + "disabledBy": [], + "allowedFunctionTypes": [], + "expression": { + "interpolated": true, + "parameters": [ + "zoom", + "measure-light" + ] + }, + "transition": true, + "mbx": { + "fullName": "paint-hillshade-hillshade-accent-color", + "name": "hillshade-accent-color", + "namespace": "paint" + } + } + ] + }, "Image": { "description": "", "displayName": "Image", diff --git a/ios/RNMBX/RNMBXHillshadeLayer.swift b/ios/RNMBX/RNMBXHillshadeLayer.swift new file mode 100644 index 000000000..bcca74584 --- /dev/null +++ b/ios/RNMBX/RNMBXHillshadeLayer.swift @@ -0,0 +1,96 @@ +import MapboxMaps + +@objc(RNMBXHillshadeLayer) +public class RNMBXHillshadeLayer: RNMBXLayer { + typealias LayerType = HillshadeLayer + + override func makeLayer(style: Style) throws -> Layer { + var layer = LayerType(id: self.id!, source: sourceID!) + layer.source = sourceID + return layer + } + +// @{codepart-replace-start(LayerPropsCommon.codepart-swift.ejs,{layerType:"Hillshade"})} + func setCommonOptions(_ layer: inout HillshadeLayer) -> Bool { + var changed = false + + if let sourceLayerID = sourceLayerID { + layer.sourceLayer = sourceLayerID + changed = true + } + + if let sourceID = sourceID { + if !(existingLayer && sourceID == DEFAULT_SOURCE_ID) && hasSource() { + layer.source = sourceID + changed = true + } + } + + if let filter = filter, filter.count > 0 { + do { + let data = try JSONSerialization.data(withJSONObject: filter, options: .prettyPrinted) + let decodedExpression = try JSONDecoder().decode(Expression.self, from: data) + layer.filter = decodedExpression + changed = true + } catch { + Logger.log(level: .error, message: "parsing filters failed for layer \(optional: id): \(error.localizedDescription)") + } + } + + return changed + } + + override func setOptions(_ layer: inout Layer) { + super.setOptions(&layer) + if var actualLayer = layer as? LayerType { + if self.setCommonOptions(&actualLayer) { + layer = actualLayer + } + } else { + Logger.log(level: .error, message: "Expected layer type to be Hillshade but was \(type(of: layer))") + } + } + + override func apply(style : Style) throws { + try style.updateLayer(withId: id, type: LayerType.self) { (layer : inout HillshadeLayer) in + if self.styleLayer != nil { + self.setOptions(&self.styleLayer!) + } + if let styleLayer = self.styleLayer as? LayerType { + layer = styleLayer + } + } + } +// @{codepart-replace-end} + + override func addStyles() { + if let style : Style = self.style, + let reactStyle = reactStyle { + let styler = RNMBXStyle(style: self.style!) + styler.bridge = self.bridge + + if var styleLayer = self.styleLayer as? LayerType { + styler.hillshadeLayer( + layer: &styleLayer, + reactStyle: reactStyle, + oldReactStyle: oldReatStyle, + applyUpdater:{ (updater) in logged("RNMBXHillshadeLayer.updateLayer") { + try style.updateLayer(withId: self.id, type: LayerType.self) { (layer: inout LayerType) in updater(&layer) } + }}, + isValid: { return self.isAddedToMap() } + ) + self.styleLayer = styleLayer + } else { + fatalError("[xxx] layer is not hillshade layer?!!! \(optional: self.styleLayer)") + } + } + } + + func isAddedToMap() -> Bool { + return true + } + + override func layerType() -> Layer.Type { + return LayerType.self + } +} diff --git a/ios/RNMBX/RNMBXHillshadeLayerComponentView.h b/ios/RNMBX/RNMBXHillshadeLayerComponentView.h new file mode 100644 index 000000000..00330718c --- /dev/null +++ b/ios/RNMBX/RNMBXHillshadeLayerComponentView.h @@ -0,0 +1,14 @@ +#ifdef __cplusplus + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RNMBXHillshadeLayerComponentView : RCTViewComponentView +@end + +NS_ASSUME_NONNULL_END +#endif // __cplusplus diff --git a/ios/RNMBX/RNMBXHillshadeLayerComponentView.mm b/ios/RNMBX/RNMBXHillshadeLayerComponentView.mm new file mode 100644 index 000000000..8c3792cf9 --- /dev/null +++ b/ios/RNMBX/RNMBXHillshadeLayerComponentView.mm @@ -0,0 +1,73 @@ + +#import "RNMBXHillshadeLayerComponentView.h" +#import "RNMBXFabricHelpers.h" + +#import +#import +#import + +#import +#import +#import +#import + +using namespace facebook::react; + +@interface RNMBXHillshadeLayerComponentView () +@end + +@implementation RNMBXHillshadeLayerComponentView { + RNMBXHillshadeLayer *_view; +} + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load +{ + [super load]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + [self prepareView]; + } + + return self; +} + +- (void)prepareView +{ + _view = [[RNMBXHillshadeLayer alloc] init]; + _view.bridge = [RCTBridge currentBridge]; + self.contentView = _view; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &newProps = static_cast(*props); + RNMBXSetCommonLayerProps(newProps, _view); + + [super updateProps:props oldProps:oldProps]; +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + [self prepareView]; +} + +@end + +Class RNMBXHillshadeLayerCls(void) +{ + return RNMBXHillshadeLayerComponentView.class; +} diff --git a/package.json b/package.json index cdcb8a6ad..fd9a1228b 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,9 @@ "RNMBXHeatmapLayer": { "className": "RNMBXHeatmapLayerComponentView" }, + "RNMBXHillshadeLayer": { + "className": "RNMBXHillshadeLayerComponentView" + }, "RNMBXImage": { "className": "RNMBXImageComponentView" }, @@ -306,6 +309,7 @@ "RNMBXFillExtrusionLayer": "RNMBXFillExtrusionLayerComponentView", "RNMBXFillLayer": "RNMBXFillLayerComponentView", "RNMBXHeatmapLayer": "RNMBXHeatmapLayerComponentView", + "RNMBXHillshadeLayer": "RNMBXHillshadeLayerComponentView", "RNMBXImage": "RNMBXImageComponentView", "RNMBXImageSource": "RNMBXImageSourceComponentView", "RNMBXImages": "RNMBXImagesComponentView", diff --git a/src/Mapbox.native.ts b/src/Mapbox.native.ts index ae6e4a8bc..638f28a92 100644 --- a/src/Mapbox.native.ts +++ b/src/Mapbox.native.ts @@ -40,6 +40,7 @@ export { default as ModelLayer } from './components/ModelLayer'; export { SymbolLayer } from './components/SymbolLayer'; export { default as RasterLayer } from './components/RasterLayer'; export { default as RasterParticleLayer } from './components/RasterParticleLayer'; +export { default as HillshadeLayer } from './components/HillshadeLayer'; export { default as BackgroundLayer } from './components/BackgroundLayer'; export { default as CustomLocationProvider } from './components/CustomLocationProvider'; export { Terrain } from './components/Terrain'; diff --git a/src/components/HillshadeLayer.tsx b/src/components/HillshadeLayer.tsx new file mode 100644 index 000000000..332951c17 --- /dev/null +++ b/src/components/HillshadeLayer.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { NativeModules } from 'react-native'; + +import type { + FilterExpression, + HillshadeLayerStyleProps, +} from '../utils/MapboxStyles'; +import { StyleValue } from '../utils/StyleValue'; +import RNMBXHillshadeLayerNativeComponent from '../specs/RNMBXHillshadeLayerNativeComponent'; + +import AbstractLayer from './AbstractLayer'; + +const Mapbox = NativeModules.RNMBXModule; + +// @{codepart-replace-start(LayerPropsCommon.codepart-tsx)} +type Slot = 'bottom' | 'middle' | 'top'; + +type LayerPropsCommon = { + /** + * A string that uniquely identifies the source in the style to which it is added. + */ + id: string; + + /** + * The id refers to an existing layer in the style. Does not create a new layer. + */ + existing?: boolean; + + /** + * The source from which to obtain the data to style. + * If the source has not yet been added to the current style, the behavior is undefined. + * Inferred from parent source only if the layer is a direct child to it. + */ + sourceID?: string; + + /** + * Identifier of the layer within the source identified by the sourceID property from which the receiver obtains the data to style. + */ + sourceLayerID?: string; + + /** + * Inserts a layer above aboveLayerID. + */ + aboveLayerID?: string; + + /** + * Inserts a layer below belowLayerID + */ + belowLayerID?: string; + + /** + * Inserts a layer at a specified index + */ + layerIndex?: number; + + /** + * Filter only the features in the source layer that satisfy a condition that you define + */ + filter?: FilterExpression; + + /** + * The minimum zoom level at which the layer gets parsed and appears. + */ + minZoomLevel?: number; + + /** + * The maximum zoom level at which the layer gets parsed and appears. + */ + maxZoomLevel?: number; + + /** + * The slot this layer is assigned to. If specified, and a slot with that name exists, it will be placed at that position in the layer order. + * + * v11 only + */ + slot?: Slot; +}; +// @{codepart-replace-end} + +export type Props = LayerPropsCommon & { + /** + * Customizable style attributes + */ + style: HillshadeLayerStyleProps; +} & React.ComponentProps; + +type NativeTypeProps = Omit & { + reactStyle?: { [key: string]: StyleValue }; +}; + +class HillshadeLayer extends AbstractLayer { + static defaultProps = { + sourceID: Mapbox.StyleSource.DefaultSourceID, + }; + + render() { + const props = { + ...this.baseProps, + sourceLayerID: this.props.sourceLayerID, + }; + return ( + // @ts-expect-error just codegen stuff + + ); + } +} + +export default HillshadeLayer; diff --git a/src/specs/RNMBXHillshadeLayerNativeComponent.ts b/src/specs/RNMBXHillshadeLayerNativeComponent.ts new file mode 100644 index 000000000..ac9c4f755 --- /dev/null +++ b/src/specs/RNMBXHillshadeLayerNativeComponent.ts @@ -0,0 +1,39 @@ +import type { HostComponent, ViewProps } from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +// @ts-ignore - CI environment type resolution issue for CodegenTypes +import { Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes'; + +import type { FilterExpression } from '../utils/MapboxStyles'; + +import type { UnsafeMixed } from './codegenUtils'; + +// @{codepart-replace-start(CommonLayerNativeComponentsProps.codepart-ts)} +// see https://github.com/rnmapbox/maps/wiki/FabricOptionalProp +type OptionalProp = UnsafeMixed; +type Slot = 'bottom' | 'middle' | 'top'; + +type CommonProps = { + sourceID?: OptionalProp; + existing?: OptionalProp; + filter?: UnsafeMixed; + + aboveLayerID?: OptionalProp; + belowLayerID?: OptionalProp; + layerIndex?: OptionalProp; + + maxZoomLevel?: OptionalProp; + minZoomLevel?: OptionalProp; + sourceLayerID?: OptionalProp; + slot?: OptionalProp; +}; +// @{codepart-replace-end} + +export interface NativeProps extends ViewProps, CommonProps { + id?: OptionalProp; + reactStyle: UnsafeMixed; +} + +// @ts-ignore-error - Codegen requires single cast but TypeScript prefers double cast +export default codegenNativeComponent( + 'RNMBXHillshadeLayer', +) as HostComponent; From 9d7f5c4ff1f53c5ebcdfc438b580f5c7fe0d5d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Sun, 15 Feb 2026 15:42:48 +0100 Subject: [PATCH 2/2] add HillshadeLayer example (Grand Canyon) --- docs/examples.json | 13 ++++ .../FillRasterLayer/HillshadeSource.tsx | 60 +++++++++++++++++++ example/src/examples/FillRasterLayer/index.js | 1 + 3 files changed, 74 insertions(+) create mode 100644 example/src/examples/FillRasterLayer/HillshadeSource.tsx diff --git a/docs/examples.json b/docs/examples.json index 77b52e1e0..45a02129e 100644 --- a/docs/examples.json +++ b/docs/examples.json @@ -709,6 +709,19 @@ "relPath": "FillRasterLayer/GeoJSONSource.js", "name": "GeoJSONSource" }, + { + "metadata": { + "title": "Hillshade Layer", + "tags": [ + "RasterDemSource", + "HillshadeLayer" + ], + "docs": "Renders terrain hillshading from a raster-dem source." + }, + "fullPath": "example/src/examples/FillRasterLayer/HillshadeSource.tsx", + "relPath": "FillRasterLayer/HillshadeSource.tsx", + "name": "HillshadeSource" + }, { "metadata": { "title": "Image Overlay", diff --git a/example/src/examples/FillRasterLayer/HillshadeSource.tsx b/example/src/examples/FillRasterLayer/HillshadeSource.tsx new file mode 100644 index 000000000..6c70ced40 --- /dev/null +++ b/example/src/examples/FillRasterLayer/HillshadeSource.tsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { Button } from 'react-native'; +import { + MapView, + Camera, + RasterDemSource, + HillshadeLayer, +} from '@rnmapbox/maps'; + +const HillshadeSource = () => { + const [showHillshade, setShowHillshade] = useState(true); + + return ( + <> +