diff --git a/README.md b/README.md
index 2b28fd791..623b65e12 100644
--- a/README.md
+++ b/README.md
@@ -192,6 +192,7 @@ const styles = StyleSheet.create({
- [Camera](docs/Camera.md)
- [UserLocation](docs/UserLocation.md)
- [Images](docs/Images.md)
+- [Image](docs/Image.md)
### Sources
diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js
index 9a5a07e0f..723e2c0c9 100644
--- a/__tests__/interface.test.js
+++ b/__tests__/interface.test.js
@@ -40,6 +40,7 @@ describe('Public Interface', () => {
'ImageSource',
'RasterDemSource',
'Images',
+ 'Image',
// constants
'UserTrackingModes',
diff --git a/docs/Image.md b/docs/Image.md
new file mode 100644
index 000000000..0d0fbe57d
--- /dev/null
+++ b/docs/Image.md
@@ -0,0 +1,14 @@
+
+#
+
+
+## props
+| Prop | Type | Default | Required | Description |
+| ---- | :-- | :----- | :------ | :---------- |
+| name | `string` | `none` | `true` | Image name |
+| sdf | `boolean` | `none` | `false` | Make image an sdf optional - see [SDF icons](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/) |
+| stretchX | `Array` | `none` | `false` | stretch along x axis - optional |
+| stretchY | `Array` | `none` | `false` | stretch along y axis - optional |
+| children | `ReactElement` | `none` | `true` | Single react native view generating the image |
+
+
diff --git a/docs/Images.md b/docs/Images.md
index aed9761a0..5dd7cdf78 100644
--- a/docs/Images.md
+++ b/docs/Images.md
@@ -10,6 +10,6 @@ Images defines the images used in Symbol etc. layers.
| nativeAssetImages | `Array` | `none` | `false` | If you have an asset under Image.xcassets on iOS and the drawables directory on android
you can specify an array of string names with assets as the key `['pin']`.
Additionally object with keys sdf, and strechX, strechY is supported for [SDF icons](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/) |
| onImageMissing | `func` | `none` | `false` | Gets called when a Layer is trying to render an image whose key is not present in
any of the `Images` component of the Map.
*signature:*`(imageKey:string) => void` |
| id | `string` | `none` | `false` | FIX ME NO DESCRIPTION |
-| children | `ReactReactElement` | `none` | `false` | FIX ME NO DESCRIPTION |
+| children | `ReactElement \| Array> \| never` | `none` | `false` | FIX ME NO DESCRIPTION |
diff --git a/docs/docs.json b/docs/docs.json
index 3515d03b9..edddb8755 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -1916,6 +1916,50 @@
}
]
},
+ "Image": {
+ "description": "",
+ "displayName": "Image",
+ "methods": [],
+ "props": [
+ {
+ "name": "name",
+ "required": true,
+ "type": "string",
+ "default": "none",
+ "description": "Image name"
+ },
+ {
+ "name": "sdf",
+ "required": false,
+ "type": "boolean",
+ "default": "none",
+ "description": "Make image an sdf optional - see [SDF icons](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/)"
+ },
+ {
+ "name": "stretchX",
+ "required": false,
+ "type": "Array",
+ "default": "none",
+ "description": "stretch along x axis - optional"
+ },
+ {
+ "name": "stretchY",
+ "required": false,
+ "type": "Array",
+ "default": "none",
+ "description": "stretch along y axis - optional"
+ },
+ {
+ "name": "children",
+ "required": true,
+ "type": "ReactElement",
+ "default": "none",
+ "description": "Single react native view generating the image"
+ }
+ ],
+ "fileNameWithExt": "Image.tsx",
+ "name": "Image"
+ },
"ImageSource": {
"description": "ImageSource is a content source that is used for a georeferenced raster image to be shown on the map.\nThe georeferenced image scales and rotates as the user zooms and rotates the map",
"displayName": "ImageSource",
@@ -2011,7 +2055,7 @@
{
"name": "children",
"required": false,
- "type": "ReactReactElement",
+ "type": "ReactElement \\| Array> \\| never",
"default": "none",
"description": "FIX ME NO DESCRIPTION"
}
diff --git a/example/src/examples/SymbolCircleLayer/SdfIcons.tsx b/example/src/examples/SymbolCircleLayer/SdfIcons.tsx
index 1eb59c0aa..29860ca86 100644
--- a/example/src/examples/SymbolCircleLayer/SdfIcons.tsx
+++ b/example/src/examples/SymbolCircleLayer/SdfIcons.tsx
@@ -1,9 +1,9 @@
import React from 'react';
+import { View } from 'react-native';
import Mapbox from '@rnmapbox/maps';
import sheet from '../../styles/sheet';
import exampleIcon from '../../assets/example.png';
-import pinIcon from '../../assets/pin.png';
import { BaseExampleProps } from '../common/BaseExamplePropTypes';
import Page from '../common/Page';
@@ -11,7 +11,7 @@ const styles = {
icon: {
iconImage: ['get', 'icon'],
- iconColor: '#f0c',
+ iconColor: ['get', 'color'],
iconSize: [
'match',
@@ -32,7 +32,8 @@ const featureCollection: GeoJSON.FeatureCollection = {
type: 'Feature',
id: '9d10456e-bdda-4aa9-9269-04c1667d4552',
properties: {
- icon: 'example',
+ icon: 'rn-image',
+ color: '#f0c',
},
geometry: {
type: 'Point',
@@ -43,7 +44,8 @@ const featureCollection: GeoJSON.FeatureCollection = {
type: 'Feature',
id: '9d10456e-bdda-4aa9-9269-04c1667d4552',
properties: {
- icon: 'airport-15',
+ icon: 'rn-image',
+ color: '#0fc',
},
geometry: {
type: 'Point',
@@ -54,7 +56,8 @@ const featureCollection: GeoJSON.FeatureCollection = {
type: 'Feature',
id: '9d10456e-bdda-4aa9-9269-04c1667d4552',
properties: {
- icon: 'pin',
+ icon: 'rn-image',
+ color: '#cf0',
},
geometry: {
type: 'Point',
@@ -65,7 +68,8 @@ const featureCollection: GeoJSON.FeatureCollection = {
type: 'Feature',
id: '9d10456e-bdda-4aa9-9269-04c1667d4553',
properties: {
- icon: 'pin3',
+ icon: 'rn-image',
+ color: '#c00',
},
geometry: {
type: 'Point',
@@ -96,11 +100,20 @@ class SdfIcons extends React.PureComponent {
nativeAssetImages={[{ name: 'pin', sdf: true }]}
images={images}
onImageMissing={(imageKey: string) =>
- this.setState({
- images: { ...this.state.images, [imageKey]: pinIcon },
- })
+ console.log('=> on image missing', imageKey)
}
- />
+ >
+
+
+
+
diff --git a/index.d.ts b/index.d.ts
index aefce8dcf..e99b11a6c 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -47,7 +47,8 @@ import {
type UserTrackingMode as _UserTrackingMode,
type UserTrackingModeChangeCallback as _UserTrackingModeChangeCallback,
} from './javascript/components/Camera';
-import { Images as _Images } from './javascript/components/Images';
+import _Images from './javascript/components/Images';
+import _Image from './javascript/components/Image';
import { MarkerView as _MarkerView } from './javascript/components/MarkerView';
import { PointAnnotation as _PointAnnotation } from './javascript/components/PointAnnotation';
import { Atmosphere as _Atmosphere } from './javascript/components/Atmosphere';
@@ -358,6 +359,8 @@ declare namespace MapboxGL {
type Location = _Location;
type Images = _Images;
const Images = _Images;
+ type Image = _Image;
+ const Image = _Image;
/**
* Offline
@@ -780,6 +783,7 @@ export import AnimatedPoint = MapboxGL.AnimatedPoint;
export import AnimatedMapPoint = MapboxGL.AnimatedPoint;
export import AnimatedShape = MapboxGL.AnimatedShape;
export import Images = MapboxGL.Images;
+export import Image = MapboxGL.Image;
export const { offlineManager } = MapboxGL;
diff --git a/ios/RCTMGL-v10/RCMTGLImage.swift b/ios/RCTMGL-v10/RCMTGLImage.swift
new file mode 100644
index 000000000..a86222a16
--- /dev/null
+++ b/ios/RCTMGL-v10/RCMTGLImage.swift
@@ -0,0 +1,93 @@
+import MapboxMaps
+
+class RCTMGLImage : UIView {
+ @objc
+ var name: String = ""
+
+ var image: UIImage? = nil
+
+ var sdf: Bool? = nil
+ var stretchX: [[NSNumber]] = []
+ var stretchY: [[NSNumber]] = []
+
+ weak var images: RCTMGLImageSetter? = nil {
+ didSet {
+ DispatchQueue.main.async { self.setImage() }
+ }
+ }
+ weak var bridge : RCTBridge! = nil
+
+ var reactSubviews : [UIView] = []
+
+ // MARK: - subview management
+
+ @objc open override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
+ reactSubviews.insert(subview, at: atIndex)
+ if reactSubviews.count > 1 {
+ Logger.log(level: .error, message: "Image supports max 1 subview")
+ }
+ if image == nil {
+ DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10)) {
+ self.setImage()
+ }
+ }
+ }
+
+ @objc
+ open override func removeReactSubview(_ subview: UIView!) {
+ reactSubviews.removeAll(where: { $0 == subview })
+ }
+
+ // MARK: - view shnapshot
+
+ func changeImage(_ image: UIImage, name: String) {
+ if let images = images {
+ let _ = images.addImage(name: name, image: image, sdf: sdf, stretchX:stretchX, stretchY:stretchY, log: "RCTMGLImage.addImage")
+ }
+ }
+
+ func setImage() {
+ if let image = _createViewSnapshot() {
+ changeImage(image, name: name)
+ }
+ }
+
+ func _createViewSnapshot() -> UIImage? {
+ let useDummyImage = false
+ if useDummyImage {
+ let size = CGSize(width: 32, height: 32)
+ let renderer = UIGraphicsImageRenderer(size: size)
+ let image = renderer.image { context in
+ UIColor.darkGray.setStroke()
+ context.stroke(CGRect(x: 0, y:0, width: 32, height: 32))
+ UIColor(red: 158/255, green: 215/255, blue: 245/255, alpha: 1).setFill()
+ context.fill(CGRect(x: 2, y: 2, width: 30, height: 30))
+ }
+ return image
+ }
+ guard reactSubviews.count > 0 else {
+ return nil
+ }
+ return _createViewSnapshot(view: reactSubviews[0])
+ }
+
+ func _createViewSnapshot(view: UIView) -> UIImage? {
+ guard view.bounds.size.width > 0 && view.bounds.size.height > 0 else {
+ return nil
+ }
+
+ let roundUp = 4
+
+ let adjustedSize = CGSize(
+ width: ((Int(view.bounds.size.width)+roundUp-1)/roundUp)*roundUp,
+ height: ((Int(view.bounds.size.height)+roundUp-1)/roundUp)*roundUp
+ )
+
+ let renderer = UIGraphicsImageRenderer(size: adjustedSize)
+ let image = renderer.image { context in
+ view.layer.render(in: context.cgContext)
+ }
+ return image
+ }
+}
+
diff --git a/ios/RCTMGL-v10/RCMTGLImageManager.m b/ios/RCTMGL-v10/RCMTGLImageManager.m
new file mode 100644
index 000000000..3caaf3615
--- /dev/null
+++ b/ios/RCTMGL-v10/RCMTGLImageManager.m
@@ -0,0 +1,9 @@
+#import
+#import
+
+@interface RCT_EXTERN_MODULE(RCTMGLImageManager, RCTViewManager)
+
+RCT_EXPORT_VIEW_PROPERTY(name, NSString)
+
+@end
+
diff --git a/ios/RCTMGL-v10/RCMTGLImageManager.swift b/ios/RCTMGL-v10/RCMTGLImageManager.swift
new file mode 100644
index 000000000..275478b2f
--- /dev/null
+++ b/ios/RCTMGL-v10/RCMTGLImageManager.swift
@@ -0,0 +1,14 @@
+
+@objc(RCTMGLImageManager)
+class RCTMGLImageManager : RCTViewManager {
+ @objc
+ override static func requiresMainQueueSetup() -> Bool {
+ return true
+ }
+
+ override func view() -> UIView! {
+ let layer = RCTMGLImage()
+ layer.bridge = self.bridge
+ return layer
+ }
+}
diff --git a/ios/RCTMGL-v10/RCTMGLImages.swift b/ios/RCTMGL-v10/RCTMGLImages.swift
index 9b7f22a5c..706062510 100644
--- a/ios/RCTMGL-v10/RCTMGLImages.swift
+++ b/ios/RCTMGL-v10/RCTMGLImages.swift
@@ -1,16 +1,24 @@
import MapboxMaps
+protocol RCTMGLImageSetter : AnyObject {
+ func addImage(name: String, image: UIImage, sdf: Bool?, stretchX: [[NSNumber]], stretchY: [[NSNumber]], log: String) -> Bool
+}
+
class RCTMGLImages : UIView, RCTMGLMapComponent {
weak var bridge : RCTBridge! = nil
var remoteImages : [String:String] = [:]
+ weak var style: Style? = nil
+
@objc
var onImageMissing: RCTBubblingEventBlock? = nil
@objc
var images : [String:Any] = [:]
+ var imageViews: [RCTMGLImage] = []
+
@objc
var nativeImages: [Any] = [] {
didSet {
@@ -21,6 +29,23 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
typealias NativeImageInfo = (name:String, sdf: Bool, stretchX:[(from:Float, to:Float)], stretchY:[(from:Float, to:Float)]);
var nativeImageInfos: [NativeImageInfo] = []
+ @objc open override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
+ if let image = subview as? RCTMGLImage {
+ imageViews.insert(image, at: atIndex)
+ } else {
+ Logger.log(level:.warn, message: "RCTMGLImages children can only be RCTMGLImage, got \(optional: subview)")
+ }
+ super.insertReactSubview(subview, at: atIndex)
+ }
+
+ @objc open override func removeReactSubview(_ subview: UIView!) {
+ if let image = subview as? RCTMGLImage {
+ imageViews.removeAll { $0 == image }
+ image.images = nil
+ }
+ super.removeReactSubview(subview)
+ }
+
// MARK: - RCTMGLMapComponent
func waitForStyleLoad() -> Bool {
@@ -28,14 +53,17 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
}
func addToMap(_ map: RCTMGLMapView, style: Style) {
+ self.style = style
map.images.append(self)
map.setupEvents()
self.addNativeImages(style: style, nativeImages: nativeImageInfos)
self.addRemoteImages(style: style, remoteImages: images)
+ self.addImageViews(style: style, imageViews: imageViews)
}
func removeFromMap(_ map: RCTMGLMapView) {
+ self.style = nil
// v10todo
}
@@ -63,6 +91,12 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
}
}
+ private func addImageViews(style: Style, imageViews: [RCTMGLImage]) {
+ imageViews.forEach { imageView in
+ imageView.images = self
+ }
+ }
+
public func addMissingImageToStyle(style: Style, imageName: String) -> Bool {
if let nativeImage = nativeImageInfos.first(where: { $0.name == imageName }) {
addNativeImages(style: style, nativeImages: [nativeImage])
@@ -84,6 +118,16 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
}
}
+ func convert(stretch: [[NSNumber]]) -> [(from: Float, to: Float)] {
+ return stretch.map{ pair in
+ return (from: pair[0].floatValue, to: pair[1].floatValue)
+ }
+ }
+
+ func convert(stretch: [(from: Float, to: Float)]) -> [ImageStretches] {
+ return stretch.map { v in ImageStretches(first: v.from, second: v.to) }
+ }
+
func decodeImage(_ imageNameOrInfo: Any) -> NativeImageInfo? {
if let imageName = imageNameOrInfo as? String {
return (name: imageName, sdf: false, stretchX:[],stretchY:[])
@@ -100,18 +144,14 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
}
if let stretchXV = imageInfo["stretchX"] as? [[NSNumber]] {
- stretchX = stretchXV.map{ pair in
- return (from: pair[0].floatValue, to: pair[1].floatValue)
- }
+ stretchX = convert(stretch: stretchXV)
}
if let stretchYV = imageInfo["stretchY"] as? [[NSNumber]] {
- stretchY = stretchYV.map{ pair in
- return (from: pair[0].floatValue, to: pair[1].floatValue)
- }
+ stretchY = convert(stretch: stretchYV)
}
- return (name: name, sdf: sdf, stretchX:stretchX,stretchY:stretchY)
+ return (name: name, sdf: sdf, stretchX: stretchX, stretchY: stretchY)
} else {
Logger.log(level: .warn, message: "RCTMGLImage.nativeImage, unexpected image: \(imageNameOrInfo)")
return nil
@@ -125,8 +165,9 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
if let image = UIImage(named: imageName) {
logged("RCTMGLImage.addNativeImage: \(imageName)") {
try style.addImage(image, id: imageName, sdf: imageInfo.sdf,
- stretchX: imageInfo.stretchX.map { v in ImageStretches(first: v.from, second: v.to) },
- stretchY: imageInfo.stretchY.map { v in ImageStretches(first: v.from, second: v.to) } )
+ stretchX: convert(stretch: imageInfo.stretchX),
+ stretchY: convert(stretch: imageInfo.stretchY)
+ )
}
} else {
Logger.log(level:.error, message: "Cannot find nativeImage named \(imageName)")
@@ -141,5 +182,23 @@ class RCTMGLImages : UIView, RCTMGLMapComponent {
UIGraphicsEndImageContext()
return result
}()
+}
+extension RCTMGLImages : RCTMGLImageSetter {
+ func addImage(name: String, image: UIImage, sdf: Bool?, stretchX: [[NSNumber]], stretchY: [[NSNumber]], log: String) -> Bool
+ {
+ return logged("\(log).addImage") {
+ if let style = style {
+ try style.addImage(image,
+ id:name,
+ sdf: sdf ?? false,
+ stretchX: convert(stretch: convert(stretch: stretchX)),
+ stretchY: convert(stretch: convert(stretch: stretchY))
+ )
+ return true
+ } else {
+ return false
+ }
+ } ?? false
+ }
}
diff --git a/javascript/components/Image.tsx b/javascript/components/Image.tsx
new file mode 100644
index 000000000..2e77624b0
--- /dev/null
+++ b/javascript/components/Image.tsx
@@ -0,0 +1,55 @@
+import React, { memo, forwardRef, ReactElement } from 'react';
+import { requireNativeComponent } from 'react-native';
+
+interface Props {
+ /** Image name */
+ name: string;
+
+ /** Make image an sdf optional - see [SDF icons](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/) */
+ sdf?: boolean;
+
+ /** stretch along x axis - optional */
+ stretchX?: [number, number][];
+
+ /** stretch along y axis - optional */
+ stretchY?: [number, number][];
+
+ /** Single react native view generating the image */
+ children: ReactElement;
+}
+
+interface Ref {
+ refresh: () => void;
+}
+
+const Image = memo(
+ forwardRef[(function Image(
+ { name, sdf, stretchX, stretchY, children }: Props,
+ ref: React.ForwardedRef][,
+ ) {
+ const nativeProps = {
+ name,
+ sdf,
+ stretchX,
+ stretchY,
+ children,
+ };
+ return ;
+ }),
+);
+
+interface NativeProps {
+ name: string;
+ children: ReactElement;
+ sdf?: boolean;
+ stretchX?: [number, number][];
+ stretchY?: [number, number][];
+}
+
+export const NATIVE_MODULE_NAME = 'RCTMGLImage';
+
+const RCTMGLImage = requireNativeComponent(NATIVE_MODULE_NAME);
+
+Image.displayName = 'Image';
+
+export default Image;
diff --git a/javascript/components/Images.tsx b/javascript/components/Images.tsx
index b261f58f3..6d74c5b2f 100644
--- a/javascript/components/Images.tsx
+++ b/javascript/components/Images.tsx
@@ -1,8 +1,9 @@
-import React from 'react';
-import { requireNativeComponent, Image } from 'react-native';
+import React, { ReactNode, ReactElement } from 'react';
+import { requireNativeComponent, Image as RNImage } from 'react-native';
import { ImageSourcePropType, ImageResolvedAssetSource } from 'react-native';
import { ShapeSource } from './ShapeSource';
+import Image from './Image';
export const NATIVE_MODULE_NAME = 'RCTMGLImages';
@@ -23,15 +24,23 @@ function _isUrlOrPath(value: string | ImageSourcePropType): value is string {
);
}
+type TypedReactNode = ReactElement | Array> | never;
+
type NativeImage =
| string
| {
name: string;
sdf?: boolean;
- strechX: [number, number][];
- streacY: [number, number][];
+ stretchX?: [number, number][];
+ stretchY?: [number, number][];
};
+const isChildAnImage = (
+ child: ReactNode,
+): child is React.ReactElement => {
+ return React.isValidElement(child) && child.type === Image;
+};
+
interface Props {
/**
* Specifies the external images in key-value pairs required for the shape source.
@@ -54,7 +63,7 @@ interface Props {
onImageMissing?: (imageKey: string) => void;
id?: string;
- children?: React.ReactElement;
+ children?: TypedReactNode;
}
/**
@@ -85,7 +94,7 @@ class Images extends React.Component {
} else if (_isUrlOrPath(value)) {
images[imageName] = value;
} else {
- const res = Image.resolveAssetSource(value);
+ const res = RNImage.resolveAssetSource(value);
if (res && res.uri) {
images[imageName] = res;
}
@@ -93,6 +102,20 @@ class Images extends React.Component {
}
}
+ const { children } = this.props;
+ if (children) {
+ const childrenWithWrongType = React.Children.toArray(children).find(
+ (child) => !isChildAnImage(child),
+ );
+ if (childrenWithWrongType) {
+ console.error(
+ `Images component on accepts Image a children passed in: ${
+ (childrenWithWrongType as any).type || 'n/a'
+ }`,
+ );
+ }
+ }
+
if (this.props.nativeAssetImages) {
nativeImages = this.props.nativeAssetImages;
}
diff --git a/javascript/index.js b/javascript/index.js
index 966d98c87..b2cdf3091 100644
--- a/javascript/index.js
+++ b/javascript/index.js
@@ -14,6 +14,7 @@ import RasterSource from './components/RasterSource';
import RasterDemSource from './components/RasterDemSource';
import ImageSource from './components/ImageSource';
import Images from './components/Images';
+import Image from './components/Image';
import FillLayer from './components/FillLayer';
import FillExtrusionLayer from './components/FillExtrusionLayer';
import HeatmapLayer from './components/HeatmapLayer';
@@ -79,6 +80,7 @@ MapboxGL.ShapeSource = ShapeSource;
MapboxGL.RasterSource = RasterSource;
MapboxGL.ImageSource = ImageSource;
MapboxGL.Images = Images;
+MapboxGL.Image = Image;
MapboxGL.RasterDemSource = RasterDemSource;
// layers
@@ -129,6 +131,7 @@ export {
RasterDemSource,
ImageSource,
Images,
+ Image,
FillLayer,
FillExtrusionLayer,
HeatmapLayer,
]