Skip to content

Commit

Permalink
Add deferred option setRTLTextPlugin to load the plugin only when…
Browse files Browse the repository at this point in the history
… the first time RTL text is encountered. (#8865)
  • Loading branch information
Arindam Bose authored Oct 24, 2019
1 parent 5f03976 commit d69e244
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 85 deletions.
37 changes: 31 additions & 6 deletions debug/rtl.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,29 @@
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
#buttons {
position: absolute;
background: white;
top: 0px;
left: 0px;
padding: 10px;
}
</style>
</head>

<body>
<div id='map'></div>
<div id="buttons">
<button id="loadSync">Load rtlTextPlugin Sync</button><br>
<button id="loadAsync">Load rtlTextPlugin Async</button><br>
Plugin Status:<br>
<span id="pluginStatus"></span>
</div>

<script src='/dist/mapbox-gl-dev.js'></script>
<script src='/debug/access_token_generated.js'></script>
<script>
mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
var pluginURL = 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js';
var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 16.5,
Expand All @@ -26,11 +39,23 @@
hash: true
});

setTimeout(function() {
if (['loading', 'loaded'].indexOf(mapboxgl.getRTLTextPluginStatus()) === -1) {
mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
}
}, 2000);
document.getElementById('loadSync').onclick = function() {
mapboxgl.setRTLTextPlugin(pluginURL);
};

document.getElementById('loadAsync').onclick = function() {
mapboxgl.setRTLTextPlugin(pluginURL, function(err) {
if (err) {
throw err;
} else {
console.log('rtl-text-plugin loaded successfully');
}
}, true);
};

setInterval(function() {
document.getElementById('pluginStatus').innerHTML = mapboxgl.getRTLTextPluginStatus();
}, 400);
</script>
</body>
</html>
18 changes: 14 additions & 4 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {register} from '../../util/web_worker_transfer';
import EvaluationParameters from '../../style/evaluation_parameters';
import Formatted from '../../style-spec/expression/types/formatted';
import ResolvedImage from '../../style-spec/expression/types/resolved_image';
import {plugin as globalRTLTextPlugin, getRTLTextPluginStatus} from '../../source/rtl_text_plugin';

import type {
Bucket,
Expand Down Expand Up @@ -305,6 +306,7 @@ class SymbolBucket implements Bucket {
writingModes: Array<number>;
allowVerticalPlacement: boolean;
hasPaintOverrides: boolean;
hasRTLText: boolean;

constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray;
Expand All @@ -317,6 +319,7 @@ class SymbolBucket implements Bucket {
this.sourceLayerIndex = options.sourceLayerIndex;
this.hasPattern = false;
this.hasPaintOverrides = false;
this.hasRTLText = false;

const layer = this.layers[0];
const unevaluatedLayoutValues = layer._unevaluatedLayout._values;
Expand Down Expand Up @@ -409,10 +412,17 @@ class SymbolBucket implements Bucket {
// but plain string token evaluation skips that pathway so do the
// conversion here.
const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature, availableImages);
text = transformText(resolvedTokens instanceof Formatted ?
resolvedTokens :
Formatted.fromString(resolvedTokens),
layer, feature);
const formattedText = Formatted.factory(resolvedTokens);
if (formattedText.containsRTLText()) {
this.hasRTLText = true;
}
if (
!this.hasRTLText || // non-rtl text so can proceed safely
getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
this.hasRTLText && globalRTLTextPlugin.isParsed() // Use the rtlText plugin to shape text
) {
text = transformText(formattedText, layer, feature);
}
}

let icon: ResolvedImage | void;
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ const exported = {
* @function setRTLTextPlugin
* @param {string} pluginURL URL pointing to the Mapbox RTL text plugin source.
* @param {Function} callback Called with an error argument if there is an error.
* @param {boolean} lazy If set to `true`, mapboxgl will defer loading the plugin until rtl text is encountered,
* rtl text will then be rendered only after the plugin finishes loading.
* @example
* mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
* @see [Add support for right-to-left scripts](https://www.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/)
Expand Down
132 changes: 99 additions & 33 deletions src/source/rtl_text_plugin.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,143 @@
// @flow

import {Event, Evented} from '../util/evented';
import {getArrayBuffer} from '../util/ajax';
import browser from '../util/browser';
import window from '../util/window';
import assert from 'assert';
import {isWorker} from '../util/util';

const status = {
unavailable: 'unavailable',
loading: 'loading',
unavailable: 'unavailable', // Not loaded
deferred: 'deferred', // The plugin URL has been specified, but loading has been deferred
loading: 'loading', // request in-flight
loaded: 'loaded',
error: 'error'
};

export type PluginState = {
pluginStatus: $Values<typeof status>;
pluginURL: ?string,
pluginBlobURL: ?string
};

type ErrorCallback = (error: ?Error) => void;
type PluginStateSyncCallback = (state: PluginState) => void;
let _completionCallback = null;

//Variables defining the current state of the plugin
let pluginStatus = status.unavailable;
let pluginURL = null;
let pluginBlobURL = null;

export const evented = new Evented();
export const triggerPluginCompletionEvent = function(error: ?Error) {
if (_completionCallback) {
_completionCallback(error);
}
};

type CompletionCallback = (error?: Error) => void;
type ErrorCallback = (error: Error) => void;
function sendPluginStateToWorker() {
evented.fire(new Event('pluginStateChange', {pluginStatus, pluginURL, pluginBlobURL}));
}

let _completionCallback;
export const evented = new Evented();

export const getRTLTextPluginStatus = function () {
return pluginStatus;
};

export const registerForPluginAvailability = function(
callback: (args: {pluginURL: string, completionCallback: CompletionCallback}) => void
) {
if (pluginURL) {
callback({pluginURL, completionCallback: _completionCallback});
} else {
evented.once('pluginAvailable', callback);
}
export const registerForPluginStateChange = function(callback: PluginStateSyncCallback) {
// Do an initial sync of the state
callback({pluginStatus, pluginURL, pluginBlobURL});
// Listen for all future state changes
evented.on('pluginStateChange', callback);
return callback;
};

export const clearRTLTextPlugin = function() {
pluginStatus = status.unavailable;
pluginURL = null;
if (pluginBlobURL) {
window.URL.revokeObjectURL(pluginBlobURL);
}
pluginBlobURL = null;
};

export const setRTLTextPlugin = function(url: string, callback: ErrorCallback) {
if (pluginStatus === status.loading || pluginStatus === status.loaded) {
export const setRTLTextPlugin = function(url: string, callback: ?ErrorCallback, deferred: boolean = false) {
if (pluginStatus === status.deferred || pluginStatus === status.loading || pluginStatus === status.loaded) {
throw new Error('setRTLTextPlugin cannot be called multiple times.');
}
pluginStatus = status.loading;
pluginURL = browser.resolveURL(url);
_completionCallback = (error?: Error) => {
if (error) {
// Clear loaded state to allow retries
clearRTLTextPlugin();
pluginStatus = status.error;
if (callback) {
callback(error);
pluginStatus = status.deferred;
_completionCallback = callback;
sendPluginStateToWorker();

//Start downloading the plugin immediately if not intending to lazy-load
if (!deferred) {
downloadRTLTextPlugin();
}
};

export const downloadRTLTextPlugin = function() {
if (pluginStatus !== status.deferred || !pluginURL) {
throw new Error('rtl-text-plugin cannot be downloaded unless a pluginURL is specified');
}
pluginStatus = status.loading;
sendPluginStateToWorker();
if (pluginURL) {
getArrayBuffer({url: pluginURL}, (error, data) => {
if (error) {
triggerPluginCompletionEvent(error);
} else {
const rtlBlob = new window.Blob([data], {type: 'application/javascript'});
pluginBlobURL = window.URL.createObjectURL(rtlBlob);
pluginStatus = status.loaded;
sendPluginStateToWorker();
}
} else {
// Called once for each worker
pluginStatus = status.loaded;
}
};
evented.fire(new Event('pluginAvailable', {pluginURL, completionCallback: _completionCallback}));
});
}
};

export const plugin: {
applyArabicShaping: ?Function,
processBidirectionalText: ?(string, Array<number>) => Array<string>,
processStyledBidirectionalText: ?(string, Array<number>, Array<number>) => Array<[string, Array<number>]>,
isLoaded: () => boolean
isLoaded: () => boolean,
isLoading: () => boolean,
setState: (state: PluginState) => void,
isParsed: () => boolean,
getURLs: () => { blob: ?string, host: ?string }
} = {
applyArabicShaping: null,
processBidirectionalText: null,
processStyledBidirectionalText: null,
isLoaded() {
return pluginStatus === status.loaded || // Foreground: loaded if the completion callback returned successfully
plugin.applyArabicShaping != null; // Background: loaded if the plugin functions have been compiled
return pluginStatus === status.loaded || // Main Thread: loaded if the completion callback returned successfully
plugin.applyArabicShaping != null; // Web-worker: loaded if the plugin functions have been compiled
},
isLoading() { // Main Thread Only: query the loading status, this function does not return the correct value in the worker context.
return pluginStatus === status.loading;
},
setState(state: PluginState) { // Worker thread only: this tells the worker threads that the plugin is available on the Main thread
assert(isWorker(), 'Cannot set the state of the rtl-text-plugin when not in the web-worker context');

pluginStatus = state.pluginStatus;
pluginURL = state.pluginURL;
pluginBlobURL = state.pluginBlobURL;
},
isParsed(): boolean {
assert(isWorker(), 'rtl-text-plugin is only parsed on the worker-threads');

return plugin.applyArabicShaping != null &&
plugin.processBidirectionalText != null &&
plugin.processStyledBidirectionalText != null;
},
getURLs(): { blob: ?string, host: ?string } {
assert(isWorker(), 'rtl-text-plugin urls can only be queried from the worker threads');

return {
blob: pluginBlobURL,
host: pluginURL,
};
}
};
15 changes: 15 additions & 0 deletions src/source/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class Tile {

symbolFadeHoldUntil: ?number;
hasSymbolBuckets: boolean;
hasRTLText: boolean;

/**
* @param {OverscaledTileID} tileID
Expand All @@ -110,6 +111,7 @@ class Tile {
this.expirationTime = null;
this.queryPadding = 0;
this.hasSymbolBuckets = false;
this.hasRTLText = false;

// Counts the number of times a response was already expired when
// received. We're using this to add a delay when making a new request
Expand Down Expand Up @@ -184,6 +186,19 @@ class Tile {
}
}

this.hasRTLText = false;
if (this.hasSymbolBuckets) {
for (const id in this.buckets) {
const bucket = this.buckets[id];
if (bucket instanceof SymbolBucket) {
if (bucket.hasRTLText) {
this.hasRTLText = true;
break;
}
}
}
}

this.queryPadding = 0;
for (const id in this.buckets) {
const bucket = this.buckets[id];
Expand Down
10 changes: 10 additions & 0 deletions src/source/vector_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TileBounds from './tile_bounds';
import {ResourceType} from '../util/ajax';
import browser from '../util/browser';
import {cacheEntryPossiblyAdded} from '../util/tile_request_cache';
import {plugin as rtlTextPlugin, getRTLTextPluginStatus, downloadRTLTextPlugin} from './rtl_text_plugin';

import type {Source} from './source';
import type {OverscaledTileID} from './tile_id';
Expand Down Expand Up @@ -153,6 +154,15 @@ class VectorTileSource extends Evented implements Source {
if (this.map._refreshExpiredTiles && data) tile.setExpiryData(data);
tile.loadVectorData(data, this.map.painter);
if (tile.hasRTLText) {
const plugin = rtlTextPlugin;
if (!plugin.isLoading() &&
!plugin.isLoaded() &&
getRTLTextPluginStatus() === 'deferred'
) {
downloadRTLTextPlugin();
}
}
cacheEntryPossiblyAdded(this.dispatcher);
Expand Down
22 changes: 15 additions & 7 deletions src/source/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
import type {WorkerGlobalScopeInterface} from '../util/web_worker';
import type {Callback} from '../types/callback';
import type {LayerSpecification} from '../style-spec/types';
import type {PluginState} from './rtl_text_plugin';

/**
* @private
Expand Down Expand Up @@ -59,8 +60,9 @@ export default class Worker {
this.workerSourceTypes[name] = WorkerSource;
};

// This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed.
this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => {
if (globalRTLTextPlugin.isLoaded()) {
if (globalRTLTextPlugin.isParsed()) {
throw new Error('RTL text plugin already registered.');
}
globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping;
Expand Down Expand Up @@ -151,13 +153,19 @@ export default class Worker {
}
}

loadRTLTextPlugin(map: string, pluginURL: string, callback: Callback<void>) {
syncRTLPluginState(map: string, state: PluginState, callback: Callback<boolean>) {
try {
if (!globalRTLTextPlugin.isLoaded()) {
this.self.importScripts(pluginURL);
callback(globalRTLTextPlugin.isLoaded() ?
null :
new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`));
globalRTLTextPlugin.setState(state);
const {blob, host} = globalRTLTextPlugin.getURLs();
if (
globalRTLTextPlugin.isLoaded() &&
!globalRTLTextPlugin.isParsed() &&
blob != null && host != null // Not possible when `isLoaded` is true, but keeps flow happy
) {
this.self.importScripts(blob);
const complete = globalRTLTextPlugin.isParsed();
const error = complete ? undefined : new Error(`RTL Text Plugin failed to import scripts from ${host}`);
callback(error, complete);
}
} catch (e) {
callback(e.toString());
Expand Down
Loading

0 comments on commit d69e244

Please sign in to comment.