Skip to content

Commit

Permalink
Support for sprites on serve (#143)
Browse files Browse the repository at this point in the history
* Support for sprites on serve

Also creates a simple test for charites serve

* Update documentation
  • Loading branch information
keichan34 authored Dec 20, 2022
1 parent f6f0ced commit e4af1d1
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 32 deletions.
14 changes: 9 additions & 5 deletions docs/source/usage/commandline_interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ Realtime editor on browser
serve your map locally
Options:
--provider [provider] your map service. e.g. `mapbox`, `geolonia`
--mapbox-access-token [mapboxAccessToken] Access Token for the Mapbox
--port [port] Specify custom port
-h, --help display help for command
--provider [provider] your map service. e.g. `mapbox`, `geolonia`
--mapbox-access-token [mapboxAccessToken] Access Token for the Mapbox
-i, --sprite-input [<icon input directory>] directory path of icon source to build icons. The default <icon
source> is `icons/`
--port [port] Specify custom port
-h, --help display help for command
Charites has three options for `serve` command.
Expand All @@ -108,4 +110,6 @@ Charites has three options for `serve` command.
- ``--mapbox-access-token`` - Set your access-token when styling for Mapbox.
- ``--port`` - Set http server's port number. When not specified, use 8080 port.
- ``--sprite-input`` - If you are building icon spritesheets with Charites, you can specify the directory of SVG files to compile here. See the ``build`` command for more information.
- ``--port`` - Set http server's port number. When not specified, the default is 8080.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"dependencies": {
"@mapbox/mapbox-gl-style-spec": "^13.22.0",
"@maplibre/maplibre-gl-style-spec": "^14.0.2",
"@unvt/sprite-one": "^0.0.8",
"@types/jsonminify": "^0.4.1",
"@unvt/sprite-one": "^0.0.8",
"axios": "^0.24.0",
"commander": "^8.2.0",
"glob": "^7.2.0",
Expand Down Expand Up @@ -50,6 +50,7 @@
"fs-extra": "^10.0.0",
"kill-port-process": "^3.0.1",
"mocha": "^8.0.1",
"node-abort-controller": "^3.0.1",
"prettier": "^2.5.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.5"
Expand Down
4 changes: 1 addition & 3 deletions provider/default/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
if (zoom) options.zoom = zoom
const map = new maplibregl.Map(options)

window._charites.initializeWebSocket((message) => {
map.setStyle(JSON.parse(message.data))
})
window._charites.initializeWebSocket(map)

map.addControl(new maplibregl.NavigationControl(), 'top-right')

Expand Down
13 changes: 11 additions & 2 deletions provider/default/shared.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
;(async () => {
window._charites = {
initializeWebSocket: function (onmessage) {
initializeWebSocket: function (map) {
const socket = new WebSocket(`ws://${window.location.host}`)

socket.addEventListener('message', onmessage)
socket.addEventListener('message', (message) => {
const data = JSON.parse(message.data)
if (data.event === 'styleUpdate') {
map.setStyle(`http://${window.location.host}/style.json`)
} else if (data.event === 'spriteUpdate') {
map.setStyle(`http://${window.location.host}/style.json`, {
diff: false,
})
}
})
},
parseMapStyle: async function () {
const mapStyleUrl = `http://${window.location.host}/style.json`
Expand Down
4 changes: 1 addition & 3 deletions provider/geolonia/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
if (zoom) options.zoom = zoom
const map = new geolonia.Map(options)

window._charites.initializeWebSocket((message) => {
map.setStyle(JSON.parse(message.data))
})
window._charites.initializeWebSocket(map)

map.addControl(
new MaplibreLegendControl(
Expand Down
4 changes: 1 addition & 3 deletions provider/mapbox/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
if (zoom) options.zoom = zoom
const map = new mapboxgl.Map(options)

window._charites.initializeWebSocket((message) => {
map.setStyle(JSON.parse(message.data))
})
window._charites.initializeWebSocket(map)

map.addControl(new mapboxgl.NavigationControl(), 'top-right')

Expand Down
11 changes: 9 additions & 2 deletions src/cli/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@ program
'--mapbox-access-token [mapboxAccessToken]',
'Your Mapbox Access Token (required if using the `mapbox` provider)',
)
.option(
'-i, --sprite-input [<icon input directory>]',
'directory path of icon source to build icons. The default <icon source> is `icons/`',
)
.option('--port [port]', 'Specify custom port')
.action((source: string, serveOptions: serveOptions) => {
.option('--no-open', "Don't open the preview in the default browser")
.action(async (source: string, serveOptions: serveOptions) => {
const options: serveOptions = program.opts()
options.provider = serveOptions.provider
options.mapboxAccessToken = serveOptions.mapboxAccessToken
options.port = serveOptions.port
options.spriteInput = serveOptions.spriteInput
options.open = serveOptions.open
if (!fs.existsSync(defaultSettings.configFile)) {
fs.writeFileSync(
defaultSettings.configFile,
`provider: ${options.provider || 'default'}`,
)
}
try {
serve(source, program.opts())
await serve(source, program.opts())
} catch (e) {
error(e)
}
Expand Down
91 changes: 80 additions & 11 deletions src/commands/serve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'path'
import fs from 'fs'
import os from 'os'
import http from 'http'
import open from 'open'
import { WebSocketServer } from 'ws'
Expand All @@ -8,14 +9,17 @@ import watch from 'node-watch'
import { parser } from '../lib/yaml-parser'
import { validateStyle } from '../lib/validate-style'
import { defaultValues } from '../lib/defaultValues'
import { buildSprite } from '../lib/build-sprite'

export interface serveOptions {
provider?: string
mapboxAccessToken?: string
port?: string
spriteInput?: string
open?: boolean
}

export function serve(source: string, options: serveOptions) {
export async function serve(source: string, options: serveOptions) {
let port = process.env.PORT || 8080
if (options.port) {
port = Number(options.port)
Expand All @@ -42,11 +46,44 @@ export function serve(source: string, options: serveOptions) {
throw `Provider is mapbox, but the Mapbox Access Token is not set. Please provide it using --mapbox-access-token, or set it in \`~/.charites/config.yml\` (see the Global configuration section of the documentation for more information)`
}

const server = http.createServer((req, res) => {
let spriteOut: string | undefined = undefined
let spriteRefresher: (() => Promise<void>) | undefined = undefined
if (options.spriteInput) {
spriteOut = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'charites-'))
spriteRefresher = async () => {
if (
typeof options.spriteInput === 'undefined' ||
typeof spriteOut === 'undefined'
) {
return
}
await buildSprite(options.spriteInput, spriteOut, 'sprite')
}
await spriteRefresher()
}

const server = http.createServer(async (req, res) => {
const url = (req.url || '').replace(/\?.*/, '')
const defaultProviderDir = path.join(defaultValues.providerDir, 'default')
const providerDir = path.join(defaultValues.providerDir, provider)

if (
typeof spriteOut !== 'undefined' &&
url.match(/^\/sprite(@2x)?\.(json|png)/)
) {
res.statusCode = 200
if (url.endsWith('.json')) {
res.setHeader('Content-Type', 'application/json; charset=UTF-8')
} else {
res.setHeader('Content-Type', 'image/png')
}
res.setHeader('Cache-Control', 'no-store')
const filename = path.basename(url)
const fsStream = fs.createReadStream(path.join(spriteOut, filename))
fsStream.pipe(res)
return
}

switch (url) {
case '/':
res.statusCode = 200
Expand All @@ -61,12 +98,18 @@ export function serve(source: string, options: serveOptions) {
let style
try {
style = parser(sourcePath)
if (typeof spriteOut !== 'undefined') {
style.sprite = `http://${
req.headers.host || `localhost:${port}`
}/sprite`
}
validateStyle(style, provider)
} catch (error) {
console.log(error)
}
res.statusCode = 200
res.setHeader('Content-Type', 'application/json; charset=UTF-8')
res.setHeader('Cache-Control', 'no-store')
res.end(JSON.stringify(style))
break
case '/app.css':
Expand Down Expand Up @@ -116,30 +159,56 @@ export function serve(source: string, options: serveOptions) {
console.log(`Provider: ${provider}`)
console.log(`Loading your style: ${sourcePath}`)
console.log(`Your map is running on http://localhost:${port}/\n`)
open(`http://localhost:${port}`)
if (options.open) {
open(`http://localhost:${port}`)
}
})

const wss = new WebSocketServer({ server })

wss.on('connection', (ws) => {
watch(
const watcher = watch(
path.dirname(sourcePath),
{ recursive: true, filter: /\.yml$/ },
{ recursive: true, filter: /\.yml$|\.svg$/i },
(event, file) => {
console.log(`${(event || '').toUpperCase()}: ${file}`)
try {
const style = parser(sourcePath)
try {
validateStyle(style, provider)
} catch (error) {
console.log(error)
if (file?.toLowerCase().endsWith('.yml')) {
ws.send(
JSON.stringify({
event: 'styleUpdate',
}),
)
} else if (
file?.toLowerCase().endsWith('.svg') &&
typeof spriteRefresher !== 'undefined'
) {
spriteRefresher().then(() => {
ws.send(
JSON.stringify({
event: 'spriteUpdate',
}),
)
})
}
ws.send(JSON.stringify(style))
} catch (e) {
// Nothing to do
}
},
)
ws.on('close', () => {
watcher.close()
})
})

process.on('SIGINT', () => {
console.log('Cleaning up...')
server.close()
if (typeof spriteOut !== 'undefined') {
fs.rmSync(spriteOut, { recursive: true })
spriteOut = undefined
}
process.exit(0)
})

return server
Expand Down
Loading

0 comments on commit e4af1d1

Please sign in to comment.