From 281b7198246444ad152e6fb0e4d288f163632fd5 Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Thu, 10 Dec 2020 21:11:53 +0100 Subject: [PATCH 1/8] feat: add raster source & layer --- src/layers/layer-factory.android.ts | 88 +++++++++++++++++++++++++---- src/layers/layer-factory.ios.ts | 76 ++++++++++++++++++++++++- src/mapbox.android.ts | 18 +++++- src/mapbox.common.ts | 34 ++++++++++- src/mapbox.ios.ts | 37 ++++++++++-- 5 files changed, 230 insertions(+), 23 deletions(-) diff --git a/src/layers/layer-factory.android.ts b/src/layers/layer-factory.android.ts index 7a19cc7..e453147 100644 --- a/src/layers/layer-factory.android.ts +++ b/src/layers/layer-factory.android.ts @@ -21,11 +21,14 @@ export class LayerFactory { case 'symbol': nativeLayer = new com.mapbox.mapboxsdk.style.layers.SymbolLayer(style.id, sourceId).withProperties(layerProperties); break; + case 'raster': + nativeLayer = new com.mapbox.mapboxsdk.style.layers.RasterLayer(style.id, sourceId).withProperties(layerProperties); + break; default: throw new Error(`Unknown layer type: ${style.type}`); } - var layer = new Layer(nativeLayer); + const layer = new Layer(nativeLayer); return layer; } @@ -40,6 +43,8 @@ export class LayerFactory { return this.parsePropertiesForFillLayer(propertiesObject); case 'symbol': return this.parsePropertiesForSymbolLayer(propertiesObject); + case 'raster': + return this.parsePropertiesForRasterLayer(propertiesObject); default: throw new Error(`Unknown layer type: ${layerType}`); } @@ -246,15 +251,7 @@ export class LayerFactory { base = propertiesObject['circle-radius'].stops.base; } - circleProperties.push( - PropertyFactory.circleRadius( - Expression.interpolate( - Expression.exponential(new java.lang.Float(base)), - Expression.zoom(), - stopArgs - ) - ) - ); + circleProperties.push(PropertyFactory.circleRadius(Expression.interpolate(Expression.exponential(new java.lang.Float(base)), Expression.zoom(), stopArgs))); } } @@ -443,7 +440,76 @@ export class LayerFactory { if (propertiesObject['visibility']) { symbolProperties.push(PropertyFactory.visibility(propertiesObject['visibility'])); } - + return symbolProperties; } + + private static parsePropertiesForRasterLayer(propertiesObject) { + const rasterProperties = []; + + if (!propertiesObject) { + return rasterProperties; + } + + /* + raster-brightness-max ✓ + raster-brightness-min ✓ + raster-contrast ✓ + raster-fade-duration ✓ + raster-hue-rotate ✓ + raster-opacity ✓ + raster-resampling ✓ + raster-saturation ✓ + visibility ✓ + */ + + const PropertyFactory = com.mapbox.mapboxsdk.style.layers.PropertyFactory; + + if (propertiesObject['raster-brightness-max']) { + rasterProperties.push(PropertyFactory.rasterBrightnessMax(new java.lang.Float(propertiesObject['raster-brightness-max']))); + } + + if (propertiesObject['raster-brightness-min']) { + rasterProperties.push(PropertyFactory.rasterBrightnessMin(new java.lang.Float(propertiesObject['raster-brightness-min']))); + } + + if (propertiesObject['raster-contrast']) { + rasterProperties.push(PropertyFactory.rasterContrast(new java.lang.Float(propertiesObject['raster-contrast']))); + } + + if (propertiesObject['raster-fade-duration']) { + rasterProperties.push(PropertyFactory.rasterFadeDuration(new java.lang.Float(propertiesObject['raster-fade-duration']))); + } + + if (propertiesObject['raster-hue-rotate']) { + rasterProperties.push(PropertyFactory.rasterHueRotate(new java.lang.Float(propertiesObject['raster-hue-rotate']))); + } + + if (propertiesObject['raster-opacity']) { + rasterProperties.push(PropertyFactory.rasterOpacity(new java.lang.Float(propertiesObject['raster-opacity']))); + } + + if (propertiesObject['raster-resampling']) { + switch (propertiesObject['raster-resampling']) { + case 'linear': + rasterProperties.push(com.mapbox.mapboxsdk.style.layers.Property.RASTER_RESAMPLING_LINEAR); + break; + case 'nearest': + rasterProperties.push(com.mapbox.mapboxsdk.style.layers.Property.RASTER_RESAMPLING_NEAREST); + break; + default: + throw new Error('Unknown raster resampling value.'); + } + } + + if (propertiesObject['raster-saturation']) { + rasterProperties.push(PropertyFactory.rasterSaturation(new java.lang.Float(propertiesObject['raster-saturation']))); + } + + if (propertiesObject['visibility']) { + rasterProperties.push(PropertyFactory.visibility(propertiesObject['visibility'])); + } + + return rasterProperties; + } } diff --git a/src/layers/layer-factory.ios.ts b/src/layers/layer-factory.ios.ts index 344ee42..072a148 100644 --- a/src/layers/layer-factory.ios.ts +++ b/src/layers/layer-factory.ios.ts @@ -19,6 +19,9 @@ export class LayerFactory { case 'symbol': nativeLayer = MGLSymbolStyleLayer.alloc().initWithIdentifierSource(style.id, source); break; + case 'raster': + nativeLayer = MGLRasterStyleLayer.alloc().initWithIdentifierSource(style.id, source); + break; default: throw new Error(`Unknown layer type: ${style.type}`); } @@ -31,12 +34,12 @@ export class LayerFactory { } } - var layer = new Layer(nativeLayer); + const layer = new Layer(nativeLayer); return layer; } - private static parseProperties(layerType, propertiesObject) { + private static parseProperties(layerType, propertiesObject): any { switch (layerType) { case 'line': return this.parsePropertiesForLineLayer(propertiesObject); @@ -46,6 +49,8 @@ export class LayerFactory { return this.parsePropertiesForFillLayer(propertiesObject); case 'symbol': return this.parsePropertiesForSymbolLayer(propertiesObject); + case 'raster': + return this.parsePropertiesForRasterLayer(propertiesObject); default: throw new Error(`Unknown layer type: ${layerType}`); } @@ -360,4 +365,71 @@ export class LayerFactory { return symbolProperties; } + + private static parsePropertiesForRasterLayer(propertiesObject) { + const rasterProperties = []; + + if (!propertiesObject) { + return rasterProperties; + } + + /* + raster-brightness-max ✓ + raster-brightness-min ✓ + raster-contrast ✓ + raster-fade-duration ✓ + raster-hue-rotate ✓ + raster-opacity ✓ + raster-resampling ✓ + raster-saturation ✓ + visibility ✓ + */ + + if (propertiesObject['raster-brightness-max']) { + rasterProperties['maximumRasterBrightness'] = NSExpression.expressionForConstantValue(propertiesObject['raster-brightness-max']); + } + + if (propertiesObject['raster-brightness-min']) { + rasterProperties['minimumRasterBrightness'] = NSExpression.expressionForConstantValue(propertiesObject['raster-brightness-min']); + } + + if (propertiesObject['raster-contrast']) { + rasterProperties['rasterContrast'] = NSExpression.expressionForConstantValue(propertiesObject['raster-constrast']); + } + + if (propertiesObject['raster-fade-duration']) { + rasterProperties['rasterFadeDuration'] = NSExpression.expressionForConstantValue(propertiesObject['raster-fade-duration']); + } + + if (propertiesObject['raster-hue-rotate']) { + rasterProperties['rasterHueRotation'] = NSExpression.expressionForConstantValue(propertiesObject['raster-hue-rotate']); + } + + if (propertiesObject['raster-opacity']) { + rasterProperties['rasterOpacity'] = NSExpression.expressionForConstantValue(propertiesObject['raster-opacity']); + } + + if (propertiesObject['raster-resampling']) { + switch (propertiesObject['raster-resampling']) { + case 'linear': + rasterProperties['rasterResamplingMode'] = MGLRasterResamplingMode.Linear; + break; + case 'nearest': + rasterProperties['rasterResamplingMode'] = MGLRasterResamplingMode.Nearest; + break; + default: + throw new Error('Unknown raster resampling value.'); + } + } + + if (propertiesObject['raster-saturation']) { + rasterProperties['rasterSaturation'] = NSExpression.expressionForConstantValue(propertiesObject['raster-saturation']); + } + + if (propertiesObject['visibility']) { + rasterProperties['visibility'] = NSExpression.expressionForConstantValue(propertiesObject['visibility']); + } + + return rasterProperties; + } } diff --git a/src/mapbox.android.ts b/src/mapbox.android.ts index 9652938..ac29b0d 100755 --- a/src/mapbox.android.ts +++ b/src/mapbox.android.ts @@ -2627,7 +2627,7 @@ export class Mapbox extends MapboxCommon implements MapboxApi { addSource(id: string, options: AddSourceOptions, nativeMap?): Promise { return new Promise((resolve, reject) => { try { - const { url, type } = options; + const { type } = options; const theMap = nativeMap || this._mapboxMapInstance; let source; @@ -2641,9 +2641,9 @@ export class Mapbox extends MapboxCommon implements MapboxApi { return; } - switch (type) { + switch (options.type) { case 'vector': - source = new com.mapbox.mapboxsdk.style.sources.VectorSource(id, url); + source = new com.mapbox.mapboxsdk.style.sources.VectorSource(id, options.url); break; case 'geojson': @@ -2659,6 +2659,18 @@ export class Mapbox extends MapboxCommon implements MapboxApi { break; + case 'raster': + const tileSet = new com.mapbox.mapboxsdk.style.sources.TileSet('tileset', options.tiles); + tileSet.setMinZoom(options.minzoom); + tileSet.setMaxZoom(options.maxzoom); + tileSet.setScheme(options.scheme); + + if (options.bounds) { + tileSet.setBounds(options.bounds.map((val) => new java.lang.Float(val))); + } + + source = new com.mapbox.mapboxsdk.style.sources.RasterSource(id, tileSet, options.tileSize); + break; default: reject('Invalid source type: ' + type); return; diff --git a/src/mapbox.common.ts b/src/mapbox.common.ts index 4fa270d..f1ec25c 100755 --- a/src/mapbox.common.ts +++ b/src/mapbox.common.ts @@ -1,4 +1,4 @@ -import { Color, ContentView, Property, Trace, booleanConverter, ImageSource } from '@nativescript/core'; +import { Color, ContentView, Property, Trace, booleanConverter } from '@nativescript/core'; export const MapboxTraceCategory = 'NativescriptMapbox'; export enum CLogTypes { @@ -285,9 +285,37 @@ export type UserTrackingMode = 'NONE' | 'FOLLOW' | 'FOLLOW_WITH_HEADING' | 'FOLL // ------------------------------------------------------------- -export interface AddSourceOptions { +export type AddSourceOptions = VectorSource | GeoJSONSource | RasterSource; + +// ------------------------------------------------------------- + +export interface Source { + type: 'vector' | 'raster' | 'geojson'; +} + +// ------------------------------------------------------------- + +export interface RasterSource extends Source { + type: 'raster'; + tiles: string[]; + bounds?: number[]; + minzoom?: number; + maxzoom?: number; + tileSize?: number; + scheme?: 'xyz' | 'tms'; +} + +// ------------------------------------------------------------- + +export interface VectorSource extends Source { + type: 'vector'; url: string; - type: string; +} + +// ------------------------------------------------------------- + +export interface GeoJSONSource extends Source { + type: 'geojson'; data?: any; } diff --git a/src/mapbox.ios.ts b/src/mapbox.ios.ts index e59bd49..23f3bbd 100755 --- a/src/mapbox.ios.ts +++ b/src/mapbox.ios.ts @@ -2110,7 +2110,7 @@ export class Mapbox extends MapboxCommon implements MapboxApi { addSource(id: string, options: AddSourceOptions, nativeMap?): Promise { return new Promise((resolve, reject) => { try { - const { url, type } = options; + const { type } = options; const theMap: MGLMapView = nativeMap || this._mapboxViewInstance; let source; @@ -2124,9 +2124,9 @@ export class Mapbox extends MapboxCommon implements MapboxApi { return; } - switch (type) { + switch (options.type) { case 'vector': - source = MGLVectorTileSource.alloc().initWithIdentifierConfigurationURL(id, NSURL.URLWithString(url)); + source = MGLVectorTileSource.alloc().initWithIdentifierConfigurationURL(id, NSURL.URLWithString(options.url)); break; case 'geojson': @@ -2138,11 +2138,40 @@ export class Mapbox extends MapboxCommon implements MapboxApi { const content: NSString = NSString.stringWithString(JSON.stringify(options.data)); const nsData: NSData = content.dataUsingEncoding(NSUTF8StringEncoding); const geoJsonShape = MGLShape.shapeWithDataEncodingError(nsData, NSUTF8StringEncoding); - + source = MGLShapeSource.alloc().initWithIdentifierShapeOptions(id, geoJsonShape, null); break; + case 'raster': + const sourceOptions: any = { + [MGLTileSourceOptionMinimumZoomLevel]: options.minzoom, + [MGLTileSourceOptionMaximumZoomLevel]: options.maxzoom, + [MGLTileSourceOptionTileSize]: options.tileSize + }; + + if (options.scheme) { + switch (options.scheme || 'xyz') { + case 'xyz': + sourceOptions[MGLTileSourceOptionTileCoordinateSystem] = MGLTileCoordinateSystem.XYZ; + break; + case 'tms': + sourceOptions[MGLTileSourceOptionTileCoordinateSystem] = MGLTileCoordinateSystem.TMS; + break; + default: + throw new Error('Unknown raster tile scheme.'); + } + } + if (options.bounds) { + source[MGLTileSourceOptionCoordinateBounds] = { + sw: CLLocationCoordinate2DMake(options.bounds[1], options.bounds[0]), + ne: CLLocationCoordinate2DMake(options.bounds[3], options.bounds[2]) + } as MGLCoordinateBounds; + } + + source = MGLRasterTileSource.alloc().initWithIdentifierTileURLTemplatesOptions(id, options.tiles, sourceOptions); + + break; default: reject('Invalid source type: ' + type); return; From c5a3a4cea361caee11d18e3352a6c781b4adb15e Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Thu, 10 Dec 2020 21:15:26 +0100 Subject: [PATCH 2/8] chore: update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 36ad8a1..5215d7a 100755 --- a/README.md +++ b/README.md @@ -574,6 +574,11 @@ The map will continuously move along with the last known location. https://docs.mapbox.com/mapbox-gl-js/api/#map#addsource +Supported source types: + - Vector + - GeoJson + - Raster + Adds a vector to GeoJSON source to the map. ```js @@ -614,6 +619,7 @@ Supported layer types: - Circle - Fill - Symbol + - Raster To add a line: From f849666c5d8dfb548d45da84c638d096442d2f87 Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Thu, 10 Dec 2020 22:57:57 +0100 Subject: [PATCH 3/8] fix: exception on layer factory if no paint or layout set --- src/layers/layer-factory.android.ts | 4 ++-- src/layers/layer-factory.ios.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/layers/layer-factory.android.ts b/src/layers/layer-factory.android.ts index e453147..7bc1d1e 100644 --- a/src/layers/layer-factory.android.ts +++ b/src/layers/layer-factory.android.ts @@ -3,7 +3,7 @@ import { LayerCommon } from '../mapbox.common'; export class LayerFactory { static async createLayer(style, source): Promise { - const layerProperties = this.parseProperties(style.type, Object.assign(style.paint, style.layout)); // TODO: handle defaults + const layerProperties = this.parseProperties(style.type, Object.assign(style.paint || {}, style.layout || {})); // TODO: handle defaults const sourceId = source.getId(); let nativeLayer: com.mapbox.mapboxsdk.style.layers.Layer; @@ -509,7 +509,7 @@ export class LayerFactory { if (propertiesObject['visibility']) { rasterProperties.push(PropertyFactory.visibility(propertiesObject['visibility'])); } - + return rasterProperties; } } diff --git a/src/layers/layer-factory.ios.ts b/src/layers/layer-factory.ios.ts index 072a148..63e2a46 100644 --- a/src/layers/layer-factory.ios.ts +++ b/src/layers/layer-factory.ios.ts @@ -26,7 +26,7 @@ export class LayerFactory { throw new Error(`Unknown layer type: ${style.type}`); } - const layerProperties = this.parseProperties(style.type, Object.assign(style.paint, style.layout)); // TODO: handle defaults + const layerProperties = this.parseProperties(style.type, Object.assign(style.paint || {}, style.layout || {})); // TODO: handle defaults for (const propKey in layerProperties) { if (Object.prototype.hasOwnProperty.call(layerProperties, propKey)) { @@ -367,7 +367,7 @@ export class LayerFactory { } private static parsePropertiesForRasterLayer(propertiesObject) { - const rasterProperties = []; + const rasterProperties = {}; if (!propertiesObject) { return rasterProperties; @@ -394,7 +394,7 @@ export class LayerFactory { } if (propertiesObject['raster-contrast']) { - rasterProperties['rasterContrast'] = NSExpression.expressionForConstantValue(propertiesObject['raster-constrast']); + rasterProperties['rasterContrast'] = NSExpression.expressionForConstantValue(propertiesObject['raster-contrast']); } if (propertiesObject['raster-fade-duration']) { @@ -429,7 +429,7 @@ export class LayerFactory { if (propertiesObject['visibility']) { rasterProperties['visibility'] = NSExpression.expressionForConstantValue(propertiesObject['visibility']); } - + return rasterProperties; } } From b38bbc4949cb3303f87a966a037c73bb8eb855ca Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Thu, 10 Dec 2020 23:07:08 +0100 Subject: [PATCH 4/8] fix: exception on addLayer if source is only id. --- src/mapbox.android.ts | 9 +++------ src/mapbox.ios.ts | 6 ++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/mapbox.android.ts b/src/mapbox.android.ts index ac29b0d..20d27fb 100755 --- a/src/mapbox.android.ts +++ b/src/mapbox.android.ts @@ -2751,17 +2751,14 @@ export class Mapbox extends MapboxCommon implements MapboxApi { let source = null; if (typeof style.source != 'string') { - this.addSource(style.id + '_source', style.source); + await this.addSource(style.id + '_source', style.source); source = theMap.getStyle().getSource(style.id + '_source'); } else { - source = style.source; + source = theMap.getStyle().getSource(style.source); } - - const layer = await LayerFactory.createLayer(style, source); + const layer = await LayerFactory.createLayer(style, source); this._mapboxMapInstance.getStyle().addLayer(layer.getNativeInstance()); - - return Promise.resolve(); } /** diff --git a/src/mapbox.ios.ts b/src/mapbox.ios.ts index 23f3bbd..3e871e4 100755 --- a/src/mapbox.ios.ts +++ b/src/mapbox.ios.ts @@ -2253,16 +2253,14 @@ export class Mapbox extends MapboxCommon implements MapboxApi { let source = null; if (typeof style.source != 'string') { - this.addSource(style.id + '_source', style.source); + await this.addSource(style.id + '_source', style.source); source = theMap.style.sourceWithIdentifier(style.id + '_source'); } else { - source = style.source; + source = theMap.style.sourceWithIdentifier(style.source); } const layer = await LayerFactory.createLayer(style, source); theMap.style.addLayer(layer.getNativeInstance()); - - return Promise.resolve(); } /** From 9152691295e910f78ec3f1f5dec1661d085a6c26 Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Thu, 10 Dec 2020 23:11:16 +0100 Subject: [PATCH 5/8] chore: cleanup addSource --- src/mapbox.android.ts | 3 +-- src/mapbox.ios.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/mapbox.android.ts b/src/mapbox.android.ts index 20d27fb..a2b0ef9 100755 --- a/src/mapbox.android.ts +++ b/src/mapbox.android.ts @@ -2627,7 +2627,6 @@ export class Mapbox extends MapboxCommon implements MapboxApi { addSource(id: string, options: AddSourceOptions, nativeMap?): Promise { return new Promise((resolve, reject) => { try { - const { type } = options; const theMap = nativeMap || this._mapboxMapInstance; let source; @@ -2672,7 +2671,7 @@ export class Mapbox extends MapboxCommon implements MapboxApi { source = new com.mapbox.mapboxsdk.style.sources.RasterSource(id, tileSet, options.tileSize); break; default: - reject('Invalid source type: ' + type); + reject('Invalid source type: ' + options['type']); return; } diff --git a/src/mapbox.ios.ts b/src/mapbox.ios.ts index 3e871e4..7588e48 100755 --- a/src/mapbox.ios.ts +++ b/src/mapbox.ios.ts @@ -2110,7 +2110,6 @@ export class Mapbox extends MapboxCommon implements MapboxApi { addSource(id: string, options: AddSourceOptions, nativeMap?): Promise { return new Promise((resolve, reject) => { try { - const { type } = options; const theMap: MGLMapView = nativeMap || this._mapboxViewInstance; let source; @@ -2173,7 +2172,7 @@ export class Mapbox extends MapboxCommon implements MapboxApi { break; default: - reject('Invalid source type: ' + type); + reject('Invalid source type: ' + options['type']); return; } From a384762bc6b9a2bcf1557a21e4d91c8c710b5738 Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Thu, 10 Dec 2020 23:52:33 +0100 Subject: [PATCH 6/8] chore: set raster options only if defined on android and fix marshal error. --- src/mapbox.android.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/mapbox.android.ts b/src/mapbox.android.ts index a2b0ef9..f36df53 100755 --- a/src/mapbox.android.ts +++ b/src/mapbox.android.ts @@ -2659,10 +2659,23 @@ export class Mapbox extends MapboxCommon implements MapboxApi { break; case 'raster': - const tileSet = new com.mapbox.mapboxsdk.style.sources.TileSet('tileset', options.tiles); - tileSet.setMinZoom(options.minzoom); - tileSet.setMaxZoom(options.maxzoom); - tileSet.setScheme(options.scheme); + // use Array.create because a marshal error throws on TileSet if options.tiles directly passed. + const tiles = Array.create(java.lang.String, options.tiles.length); + options.tiles.forEach((val, i) => tiles[i] = val); + + const tileSet = new com.mapbox.mapboxsdk.style.sources.TileSet('tileset', tiles); + + if (options.minzoom) { + tileSet.setMinZoom(options.minzoom); + } + + if (options.maxzoom) { + tileSet.setMaxZoom(options.maxzoom); + } + + if (options.scheme) { + tileSet.setScheme(options.scheme); + } if (options.bounds) { tileSet.setBounds(options.bounds.map((val) => new java.lang.Float(val))); From 9fd3e3d73817bf3c91bb62d799a29151a9f9ae44 Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Mon, 14 Dec 2020 17:59:31 +0100 Subject: [PATCH 7/8] chore: add demo for raster --- demo/app/main-page.xml | 2 ++ demo/app/main-view-model.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/demo/app/main-page.xml b/demo/app/main-page.xml index a0aea12..88dac0e 100644 --- a/demo/app/main-page.xml +++ b/demo/app/main-page.xml @@ -152,6 +152,8 @@