-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Add option to load rtl-text-plugin lazily #8865
Changes from 5 commits
f285c5f
7b55f59
ad15029
5bb6ff6
51158d9
8e03389
4fd3a66
c3688f6
ffe7dea
05db065
89b3bcb
247226e
edc94ad
767c9dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>Mapbox GL JS debug page</title> | ||
<meta charset='utf-8'> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | ||
<link rel='stylesheet' href='../dist/mapbox-gl.css' /> | ||
<style> | ||
body { margin: 0; padding: 0; } | ||
html, body, #map { height: 100%; } | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<div id='map'></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.3/mapbox-gl-rtl-text.js', null, true); | ||
|
||
var map = window.map = new mapboxgl.Map({ | ||
container: 'map', | ||
zoom: 12.5, | ||
center: [-77.01866, 38.888], | ||
style: 'mapbox://styles/mapbox/streets-v10', | ||
hash: true | ||
}); | ||
|
||
</script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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} from '../../source/rtl_text_plugin'; | ||
|
||
import type { | ||
Bucket, | ||
|
@@ -303,6 +304,7 @@ class SymbolBucket implements Bucket { | |
writingModes: Array<number>; | ||
allowVerticalPlacement: boolean; | ||
hasPaintOverrides: boolean; | ||
hasRTLText: boolean; | ||
|
||
constructor(options: BucketParameters<SymbolStyleLayer>) { | ||
this.collisionBoxArray = options.collisionBoxArray; | ||
|
@@ -315,6 +317,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; | ||
|
@@ -407,10 +410,18 @@ 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 && globalRTLTextPlugin.isLoaded() || // Use the rtlText plugin shape text | ||
!this.hasRTLText || // non-rtl terxt so can proceed safely | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is minor, but it seems like the order here should be |
||
!globalRTLTextPlugin.isAvailableInWorker() // We-doent intend to async-load the rtl text plugin, so proceed with incorrect shaping | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
) { | ||
text = transformText(formattedText, layer, feature); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be a warning emitted here so that customers don't just see a map with no labels and wonder what the heck is going on? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed the behavior now so that only when lazy loading, does it skip rendering the text. |
||
} | ||
} | ||
|
||
let icon: ResolvedImage | void; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,73 @@ | ||
// @flow | ||
|
||
import {Event, Evented} from '../util/evented'; | ||
import {getArrayBuffer} from '../util/ajax'; | ||
import browser from '../util/browser'; | ||
import window from '../util/window'; | ||
|
||
const status = { | ||
unavailable: 'unavailable', | ||
loading: 'loading', | ||
unavailable: 'unavailable', // Not loaded | ||
available: 'available', // Host url specified, but we havent started loading yet | ||
arindam1993 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
loading: 'loading', // request in-flight | ||
downloaded: 'downloaded', //plugin loaded and cached on main-thread and pluginBlobURL for worker is generated | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the difference between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I generally don't think these statuses make intuitive sense. We shouldn't be asking end users to understand the intricacies of lazy loading the RTL plugin on the main thread and worker threads in order to get a status update. I hated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed, I've changed the architecture as described in the main PR body. |
||
loaded: 'loaded', | ||
error: 'error' | ||
}; | ||
let pluginStatus = status.unavailable; | ||
let pluginURL = null; | ||
let pluginBlobURL = null; | ||
let lazy = null; | ||
|
||
// store `pluginAvailable` that have occurred before the `registerPluginAvailability` bind | ||
// so we can flush all the state updates to the workers | ||
let eventQueue = []; | ||
let _pluginAvailableCb = null; | ||
|
||
let _workerAvailable = false; | ||
|
||
export const evented = new Evented(); | ||
evented.on('pluginAvailable', (args) => { | ||
if (!_pluginAvailableCb) { | ||
eventQueue.push(args); | ||
} else { | ||
_pluginAvailableCb(args); | ||
} | ||
}); | ||
|
||
type CompletionCallback = (error?: Error) => void; | ||
type ErrorCallback = (error: Error) => void; | ||
type PluginAvailableCallback = (args: {pluginURL: ?string, lazy: ?boolean, completionCallback: CompletionCallback}) => void; | ||
|
||
let _completionCallback; | ||
|
||
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 registerForPluginAvailability = function(callback: PluginAvailableCallback) { | ||
for (const event of eventQueue) { | ||
callback(event); | ||
} | ||
eventQueue = []; | ||
|
||
_pluginAvailableCb = callback; | ||
return callback; | ||
}; | ||
|
||
export const clearRTLTextPlugin = function() { | ||
pluginStatus = status.unavailable; | ||
pluginURL = null; | ||
pluginBlobURL = null; | ||
lazy = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if |
||
}; | ||
|
||
export const setRTLTextPlugin = function(url: string, callback: ErrorCallback) { | ||
if (pluginStatus === status.loading || pluginStatus === status.loaded) { | ||
export const setRTLTextPlugin = function(url: string, callback: ?ErrorCallback, lazyLoad: ?boolean) { | ||
if (pluginStatus === status.available || pluginStatus === status.loading || pluginStatus === status.loaded) { | ||
throw new Error('setRTLTextPlugin cannot be called multiple times.'); | ||
} | ||
pluginStatus = status.loading; | ||
pluginURL = browser.resolveURL(url); | ||
lazy = !!lazyLoad; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, if we just default to |
||
pluginStatus = status.available; | ||
_completionCallback = (error?: Error) => { | ||
if (error) { | ||
// Clear loaded state to allow retries | ||
|
@@ -55,23 +78,73 @@ export const setRTLTextPlugin = function(url: string, callback: ErrorCallback) { | |
} | ||
} else { | ||
// Called once for each worker | ||
pluginStatus = status.loaded; | ||
if (!lazy) { | ||
pluginStatus = status.loaded; | ||
} | ||
} | ||
}; | ||
evented.fire(new Event('pluginAvailable', {pluginURL, completionCallback: _completionCallback})); | ||
|
||
if (lazy) { | ||
// Inform the worker-threads that we intend to load the plugin lazily later, | ||
// This is so the workers can skip RTL text parsing. | ||
evented.fire(new Event('pluginAvailable', { | ||
pluginURL: null, | ||
lazy, | ||
completionCallback: _completionCallback | ||
})); | ||
} else { | ||
downloadRTLTextPlugin(); | ||
} | ||
}; | ||
|
||
export const downloadRTLTextPlugin = function() { | ||
if (pluginStatus !== status.available) { | ||
throw new Error('rtl-text-plugin cannot be downloaded unless a pluginURL is specified'); | ||
} | ||
pluginStatus = status.loading; | ||
|
||
if (pluginURL) { | ||
getArrayBuffer({url: pluginURL}, (error, data) => { | ||
if (error || !data) { | ||
throw error; | ||
} else { | ||
const rtlBlob = new window.Blob([data], {type: 'application/javascript'}); | ||
const URL = window.URL || window.webkitURL; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may have been added in the past when Safari/Webkit didn't support |
||
pluginBlobURL = URL.createObjectURL(rtlBlob); | ||
pluginStatus = status.downloaded; | ||
evented.fire(new Event('pluginAvailable', { | ||
pluginURL: pluginBlobURL, | ||
lazy, | ||
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, | ||
markWorkerAvailable: () => void, | ||
isAvailableInWorker: () => boolean | ||
} = { | ||
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; | ||
}, | ||
markWorkerAvailable() { // Worker thread only: this tells the worker threads that the plugin is available on the Main thread | ||
_workerAvailable = true; | ||
}, | ||
isAvailableInWorker() { | ||
return !!_workerAvailable; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the expectation here is that the plugin won't be loaded right away? How would we see that? Can this be modified to fly to somewhere with RTL text label names and emit an alert when the popup is loaded so we can see it working? And can this be combined with the existing
rtl.html
debug page?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made the plugin loading interactive in the
rtl.html
page, so you can click a button and see the status live.