Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas source initialization from HTML element #6424

Merged
merged 8 commits into from
Apr 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions debug/canvas.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"canvas": {
"type": "canvas",
"canvas": "testCanvasID",
"animate": false,
"coordinates": [
[-122.51596391201019, 37.56238816766053],
[-122.51467645168304, 37.56410183312965],
Expand Down
1 change: 1 addition & 0 deletions docs/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ toc:
- VideoSource
- ImageSource
- CanvasSource
- CanvasSourceOptions
- name: Events
description: |
`Map` (and some other classes) emit events in response to user interactions or changes in state. `Evented`
Expand Down
69 changes: 2 additions & 67 deletions docs/pages/style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import md from '../components/md';
import PageShell from '../components/page_shell';
import LeftNav from '../components/left_nav';
import TopNav from '../components/top_nav';
import {highlightJavascript, highlightJSON, highlightMarkup} from '../components/prism_highlight';
import {highlightJavascript, highlightJSON} from '../components/prism_highlight';
import entries from 'object.entries';
import ref from '../../src/style-spec/reference/latest';

Expand Down Expand Up @@ -97,9 +97,6 @@ const navigation = [
},
{
"title": "video"
},
{
"title": "canvas"
}
]
},
Expand Down Expand Up @@ -223,7 +220,7 @@ const navigation = [
}
];

const sourceTypes = ['vector', 'raster', 'raster-dem', 'geojson', 'image', 'video', 'canvas'];
const sourceTypes = ['vector', 'raster', 'raster-dem', 'geojson', 'image', 'video'];
const layerTypes = ['background', 'fill', 'line', 'symbol', 'raster', 'circle', 'fill-extrusion', 'heatmap', 'hillshade'];

const {expressions, expressionGroups} = require('../components/expression-metadata');
Expand Down Expand Up @@ -874,68 +871,6 @@ export default class extends React.Component {
</tbody>
</table>
</div>

<div id='sources-canvas' className='pad2 keyline-bottom'>
<h3 className='space-bottom1'><a href='#sources-canvas' title='link to canvas'>canvas</a></h3>
<p>
A canvas source. The <code>"canvas"</code> value is the ID of the canvas element in the document.
</p>
<p>
The <code>"coordinates"</code> array contains <code>[longitude, latitude]</code> pairs for the video
corners listed in clockwise order: top left, top right, bottom right, bottom left.
</p>
<p>
If an HTML document contains a canvas such as this:
</p>
<div className='space-bottom1 clearfix'>
{highlightMarkup(`<canvas id="mycanvas" width="400" height="300" style="display: none;"/>`)}
</div>
<p>
the corresponding canvas source would be specified as follows:
</p>
<div className='space-bottom1 clearfix'>
{highlightJSON(`
"canvas": {
"type": "canvas",
"canvas": "mycanvas",
"coordinates": [
[-122.51596391201019, 37.56238816766053],
[-122.51467645168304, 37.56410183312965],
[-122.51309394836426, 37.563391708549425],
[-122.51423120498657, 37.56161849366671]
]
}`)}
</div>
<p>
This source type is available only in Mapbox GL JS. Avoid using it in styles that need to maintain
compatibility with other Mapbox Maps SDKs.
</p>
<div className='space-bottom1 clearfix'>
{ entries(ref.source_canvas).map(([name, prop], i) =>
name !== '*' && name !== 'type' &&
<Item key={i} id={`sources-canvas-${name}`} name={name} {...prop}/>)}
</div>
<table className="micro">
<thead>
<tr className='fill-light'>
<th>SDK Support</th>
<td className='center'>Mapbox GL JS</td>
<td className='center'>Android SDK</td>
<td className='center'>iOS SDK</td>
<td className='center'>macOS SDK</td>
</tr>
</thead>
<tbody>
<tr>
<td>basic functionality</td>
<td>&gt;= 0.32.0</td>
<td>Not supported</td>
<td>Not supported</td>
<td>Not supported</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

Expand Down
8 changes: 0 additions & 8 deletions flow-typed/style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,13 @@ declare type ImageSourceSpecification = {|
"coordinates": [[number, number], [number, number], [number, number], [number, number]]
|}

declare type CanvasSourceSpecification = {|
"type": "canvas",
"coordinates": [[number, number], [number, number], [number, number], [number, number]],
"animate"?: boolean,
"canvas": string
|}

declare type SourceSpecification =
| VectorSourceSpecification
| RasterSourceSpecification
| RasterDEMSourceSpecification
| GeojsonSourceSpecification
| VideoSourceSpecification
| ImageSourceSpecification
| CanvasSourceSpecification

declare type FillLayerSpecification = {|
"id": string,
Expand Down
47 changes: 43 additions & 4 deletions src/source/canvas_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,31 @@ import rasterBoundsAttributes from '../data/raster_bounds_attributes';
import VertexArrayObject from '../render/vertex_array_object';
import Texture from '../render/texture';
import { ErrorEvent } from '../util/evented';
import ValidationError from '../style-spec/error/validation_error';

import type Map from '../ui/map';
import type Dispatcher from '../util/dispatcher';
import type {Evented} from '../util/evented';

export type CanvasSourceSpecification = {|
"type": "canvas",
"coordinates": [[number, number], [number, number], [number, number], [number, number]],
"animate"?: boolean,
"canvas": string | HTMLCanvasElement
|};

/**
* A data source containing the contents of an HTML canvas.
* (See the [Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#sources-canvas) for detailed documentation of options.)
* Options to add a canvas source type to the map.
*
* @typedef {Object} CanvasSourceOptions
* @property {string} type Source type. Must be `"canvas"`.
* @property {string|HTMLCanvasElement} canvas Canvas source from which to read pixels. Can be a string representing the ID of the canvas element, or the `HTMLCanvasElement` itself.
* @property {Array<Array<number>>} coordinates Four geographical coordinates denoting where to place the corners of the canvas, specified in `[longitude, latitude]` pairs.
* @property {boolean} [animate=true] Whether the canvas source is animated. If the canvas is static (i.e. pixels do not need to be re-read on every frame), `animate` should be set to `false` to improve performance.
*/

/**
* A data source containing the contents of an HTML canvas. See {@link CanvasSourceOptions} for detailed documentation of options.
*
* @example
* // add to map
Expand Down Expand Up @@ -56,6 +73,25 @@ class CanvasSource extends ImageSource {
*/
constructor(id: string, options: CanvasSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) {
super(id, options, dispatcher, eventedParent);

// We build in some validation here, since canvas sources aren't included in the style spec:
if (!options.coordinates) {
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, 'missing required property "coordinates"')));
} else if (!Array.isArray(options.coordinates) || options.coordinates.length !== 4 ||
options.coordinates.some(c => !Array.isArray(c) || c.length !== 2 || c.some(l => typeof l !== 'number'))) {
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, '"coordinates" property must be an array of 4 longitude/latitude array pairs')));
}

if (options.animate && typeof options.animate !== 'boolean') {
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, 'optional "animate" property must be a boolean value')));
}

if (!options.canvas) {
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, 'missing required property "canvas"')));
} else if (typeof options.canvas !== 'string' && !(options.canvas instanceof window.HTMLCanvasElement)) {
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, '"canvas" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance')));
}

this.options = options;
this.animate = options.animate !== undefined ? options.animate : true;
}
Expand All @@ -75,7 +111,11 @@ class CanvasSource extends ImageSource {
*/

load() {
this.canvas = this.canvas || window.document.getElementById(this.options.canvas);
if (!this.canvas) {
this.canvas = (this.options.canvas instanceof window.HTMLCanvasElement) ?
this.options.canvas :
window.document.getElementById(this.options.canvas);
}
this.width = this.canvas.width;
this.height = this.canvas.height;

Expand Down Expand Up @@ -179,7 +219,6 @@ class CanvasSource extends ImageSource {
serialize(): Object {
return {
type: 'canvas',
canvas: this.canvas,
coordinates: this.coordinates
};
}
Expand Down
1 change: 1 addition & 0 deletions src/source/image_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import VertexArrayObject from '../render/vertex_array_object';
import Texture from '../render/texture';

import type {Source} from './source';
import type {CanvasSourceSpecification} from './canvas_source';
import type Map from '../ui/map';
import type Dispatcher from '../util/dispatcher';
import type Tile from './tile';
Expand Down
13 changes: 7 additions & 6 deletions src/style-spec/error/validation_error.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@

function ValidationError(key, value, message) {
this.message = (key ? `${key}: ` : '') + message;
export default class ValidationError {
constructor(key, value, message, identifier) {
this.message = (key ? `${key}: ` : '') + message;
if (identifier) this.identifier = identifier;

if (value !== null && value !== undefined && value.__line__) {
this.line = value.__line__;
if (value !== null && value !== undefined && value.__line__) {
this.line = value.__line__;
}
}
}

export default ValidationError;
37 changes: 1 addition & 36 deletions src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,7 @@
"source_raster_dem",
"source_geojson",
"source_video",
"source_image",
"source_canvas"
"source_image"
],
"source_vector": {
"type": {
Expand Down Expand Up @@ -397,40 +396,6 @@
}
}
},
"source_canvas": {
"type": {
"required": true,
"type": "enum",
"values": {
"canvas": {
"doc": "A canvas data source."
}
},
"doc": "The data type of the canvas source."
},
"coordinates": {
"required": true,
"doc": "Corners of canvas specified in longitude, latitude pairs.",
"type": "array",
"length": 4,
"value": {
"type": "array",
"length": 2,
"value": "number",
"doc": "A single longitude, latitude pair."
}
},
"animate": {
"type": "boolean",
"default": "true",
"doc": "Whether the canvas source is animated. If the canvas is static, `animate` should be set to `false` to improve performance."
},
"canvas": {
"type": "string",
"required": true,
"doc": "HTML ID of the canvas from which to read pixels."
}
},
"layer": {
"id": {
"type": "string",
Expand Down
11 changes: 3 additions & 8 deletions src/style-spec/validate/validate_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,14 @@ export default function validateSource(options) {
});

case 'canvas':
return validateObject({
key: key,
value: value,
valueSpec: styleSpec.source_canvas,
style: style,
styleSpec: styleSpec
});
errors.push(new ValidationError(key, null, `Please use runtime APIs to add canvas sources, rather than including them in stylesheets.`, 'source.canvas'));
return errors;

default:
return validateEnum({
key: `${key}.type`,
value: value.type,
valueSpec: {values: ['vector', 'raster', 'raster-dem', 'geojson', 'video', 'image', 'canvas']},
valueSpec: {values: ['vector', 'raster', 'raster-dem', 'geojson', 'video', 'image']},
style: style,
styleSpec: styleSpec
});
Expand Down
10 changes: 8 additions & 2 deletions src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getJSON, ResourceType } from '../util/ajax';
import { isMapboxURL, normalizeStyleURL } from '../util/mapbox';
import browser from '../util/browser';
import Dispatcher from '../util/dispatcher';
import { validateStyle, emitValidationErrors } from './validate_style';
import { validateStyle, emitValidationErrors as _emitValidationErrors } from './validate_style';
import {
getType as getSourceType,
setType as setSourceType
Expand All @@ -35,6 +35,12 @@ import PauseablePlacement from './pauseable_placement';
import ZoomHistory from './zoom_history';
import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index';

// We're skipping validation errors with the `source.canvas` identifier in order
// to continue to allow canvas sources to be added at runtime/updated in
// smart setStyle (see https://github.com/mapbox/mapbox-gl-js/pull/6424):
const emitValidationErrors = (evented: Evented, errors: ?$ReadOnlyArray<{message: string, identifier?: string}>) =>
_emitValidationErrors(evented, errors && errors.filter(error => error.identifier !== 'source.canvas'));

import type Map from '../ui/map';
import type Transform from '../geo/transform';
import type {Source} from '../source/source';
Expand Down Expand Up @@ -451,7 +457,7 @@ class Style extends Evented {
throw new Error(`The type property must be defined, but the only the following properties were given: ${Object.keys(source).join(', ')}.`);
}

const builtIns = ['vector', 'raster', 'geojson', 'video', 'image', 'canvas'];
const builtIns = ['vector', 'raster', 'geojson', 'video', 'image'];
const shouldValidate = builtIns.indexOf(source.type) >= 0;
if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return;

Expand Down
15 changes: 8 additions & 7 deletions src/style/validate_style.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type {Evented} from '../util/evented';

type ValidationError = {
message: string,
line: number
line: number,
identifier?: string
};

type Validator = (Object) => $ReadOnlyArray<ValidationError>;
Expand All @@ -19,13 +20,13 @@ export const validateFilter = (validateStyleMin.filter: Validator);
export const validatePaintProperty = (validateStyleMin.paintProperty: Validator);
export const validateLayoutProperty = (validateStyleMin.layoutProperty: Validator);

export function emitValidationErrors(emitter: Evented, errors: ?$ReadOnlyArray<{message: string}>) {
export function emitValidationErrors(emitter: Evented, errors: ?$ReadOnlyArray<{message: string, identifier?: string}>): boolean {
let hasErrors = false;
if (errors && errors.length) {
for (const {message} of errors) {
emitter.fire(new ErrorEvent(new Error(message)));
for (const error of errors) {
emitter.fire(new ErrorEvent(new Error(error.message)));
hasErrors = true;
}
return true;
} else {
return false;
}
return hasErrors;
}
3 changes: 2 additions & 1 deletion src/ui/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,8 @@ class Map extends Camera {
*
* @param {string} id The ID of the source to add. Must not conflict with existing sources.
* @param {Object} source The source object, conforming to the
* Mapbox Style Specification's [source definition](https://www.mapbox.com/mapbox-gl-style-spec/#sources).
* Mapbox Style Specification's [source definition](https://www.mapbox.com/mapbox-gl-style-spec/#sources) or
* {@link CanvasSourceOptions}.
* @fires source.add
* @returns {Map} `this`
* @see [Draw GeoJSON points](https://www.mapbox.com/mapbox-gl-js/example/geojson-markers/)
Expand Down
2 changes: 2 additions & 0 deletions src/util/evented.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export class Evented {
// console if they have no listeners.
} else if (endsWith(type, 'error')) {
console.error((event && event.error) || event || 'Empty error event');
} else if (endsWith(type, 'warning')) {
console.warn((event && event.warning) || event || 'Empty warning event');
}

return this;
Expand Down
Loading