Skip to content

Commit

Permalink
Show missing fonts/icons + auto unify font names on import
Browse files Browse the repository at this point in the history
  • Loading branch information
123FLO321 committed Feb 11, 2021
1 parent 5dfd05f commit a97c72b
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 10 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ Package.resolved
/Templates
/Markers
/TileServer
/Temp
.swiftpm/
.env
.env.development
docker-compose.development.yml
22 changes: 22 additions & 0 deletions Resources/Views/Styles.leaf
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
<th>Name</th>
<th>ID</th>
<th style="width: 100px">Type</th>
<th style="width: 100px">Fonts</th>
<th style="width: 100px">Icons</th>
<th style="width: 200px">Preview</th>
<th style="width: 100px">Actions</th>
</tr>
Expand All @@ -80,6 +82,26 @@
Local
#endif
</td>
<td style="vertical-align:middle">
#if(count(style.analysis.missingFonts) == 0):
OK
#else:
Missing:<br>
#for(font in style.analysis.missingFonts):
- #(font)<br>
#endfor
#endif
</td>
<td style="vertical-align:middle">
#if(count(style.analysis.missingIcons) == 0):
OK
#else:
Missing:<br>
#for(font in style.analysis.missingIcons):
- #(font)<br>
#endfor
#endif
</td>
<td style="padding:0">
<img src="/staticmap?style=#(style.id)&latitude=#(previewLatitude)&longitude=#(previewLongitude)&zoom=17&width=1000&height=1000&scale=1&_=#(time)" style="height: 250px; width: 250px;">
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ internal class DatasetsController {
if datasets.count == 1 {
try? FileManager.default.removeItem(atPath: self.folder + "/Combined.mbtiles")
do {
try shellOut(to: "/bin/ln", arguments: ["-s", "List/\(datasets[0]).mbtiles", "Combined.mbtiles"], at: self.folder)
try shellOut(to: "/bin/ln", arguments: ["-s", "List/\(datasets[0]).mbtiles".bashEncoded, "Combined.mbtiles"], at: self.folder)
} catch {
return request.eventLoop.makeFailedFuture("Failed to link mbtiles: \(error.localizedDescription)")
}
return request.eventLoop.future()
} else {
return request.application.threadPool.runIfActive(eventLoop: request.eventLoop) {
do {
try shellOut(to: DatasetsController.tileJoinCommand, arguments: ["--force", "-o", "Combined.mbtiles", "List/*.mbtiles"], at: self.folder)
try shellOut(to: DatasetsController.tileJoinCommand, arguments: ["--force", "-o", "Combined.mbtiles", "List/*.mbtiles".bashEncoded], at: self.folder)
} catch {
throw Abort(.internalServerError, reason: "Failed to get mbtiles: \(error.localizedDescription)")
}
Expand Down
7 changes: 3 additions & 4 deletions Sources/SwiftTileserverCache/Controller/FontsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal class FontsController {
}

#if os(macOS)
private static let buildGlyphsCommand = "/usr/local/opt/node@12/bin/node /usr/local/bin/build-glyphs"
private static let buildGlyphsCommand = "/usr/local/opt/node/bin/node /usr/local/bin/build-glyphs"
#else
private static let buildGlyphsCommand = "/usr/local/bin/build-glyphs"
#endif
Expand All @@ -36,7 +36,7 @@ internal class FontsController {
internal func add(request: Request) throws -> EventLoopFuture<Response> {
let font = try request.content.decode(SaveFont.self)
let tempFile = "\(tempFolder)/\(UUID().uuidString).\(font.file.extension ?? "tff")"
let name = font.file.filename.split(separator: ".").dropLast().joined(separator: ".")
let name = font.file.filename.split(separator: ".").dropLast().joined(separator: ".").toCamelCase
return request.fileio.writeFile(font.file.data, at: tempFile).flatMap { _ in
return self.buildGlyphs(request: request, file: tempFile, name: name).map { _ in
return Response(status: .ok)
Expand All @@ -63,8 +63,7 @@ internal class FontsController {
try? FileManager.default.removeItem(atPath: path)
do {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false)
print(file, path)
try shellOut(to: FontsController.buildGlyphsCommand, arguments: [file, path])
try shellOut(to: FontsController.buildGlyphsCommand, arguments: [file.bashEncoded, path.bashEncoded])
} catch {
try? FileManager.default.removeItem(atPath: path)
throw Abort(.internalServerError, reason: "Failed to create glyphs: \(error.localizedDescription)")
Expand Down
106 changes: 105 additions & 1 deletion Sources/SwiftTileserverCache/Controller/StylesController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ internal class StylesController {
private let tileServerURL: String
private var externalStyles: [String: Style]
private let folder: String
private let fontsController: FontsController

internal init(tileServerURL: String, externalStyles: [Style], folder: String) {
internal init(tileServerURL: String, externalStyles: [Style], folder: String, fontsController: FontsController) {
try? FileManager.default.createDirectory(atPath: folder, withIntermediateDirectories: true)
try? FileManager.default.createDirectory(atPath: "\(folder)/External", withIntermediateDirectories: false)
self.tileServerURL = tileServerURL
self.externalStyles = (externalStyles + StylesController.loadExternalStyles(folder: folder)).reduce(into: [String: Style](), { (into, style) in
into[style.id] = style
})
self.folder = folder
self.fontsController = fontsController
}

// MARK: - Routes
Expand All @@ -42,6 +44,38 @@ internal class StylesController {
return (styles + externalStyles).map { $0.removingURL }
}
}

internal func getWithAnalysis(request: Request) -> EventLoopFuture<[Style]> {
return get(request: request).flatMap({ styles in
let analysisFutures = styles.map({ style in
return self.analyse(request: request, id: style.id).map({ analysis in
return (id: style.id, analysis: analysis)
})
})
return analysisFutures.flatten(on: request.eventLoop).map { analysis in
return styles.map({ style in
var newStyle = style
newStyle.analysis = analysis.first(where: {$0.id == style.id})?.analysis
return newStyle
})
}
})
}

internal func analyse(request: Request, id: String) -> EventLoopFuture<Style.Analysis> {
return analyseUsage(request: request, id: id).flatMap({ usage in
return self.analyseAvilableIcons(request: request, id: id).flatMapThrowing({ icons in
let fonts = try self.fontsController.getFonts()
let missingIcons = usage.icons.filter({!icons.contains($0)})
let missingFonts = usage.fonts.filter({!fonts.contains($0)})
return .init(
missingFonts: missingFonts,
missingIcons: missingIcons
)
})
})

}

internal func addExternal(request: Request) throws -> EventLoopFuture<HTTPStatus> {
let style = try request.content.decode(Style.self)
Expand Down Expand Up @@ -82,6 +116,11 @@ internal class StylesController {
if newLayer["source"] as? String != nil {
newLayer["source"] = "combined"
}
if var layout = newLayer["layout"] as? [String: Any], let textFonts = layout["text-font"] as? [String] {
layout["text-font"] = textFonts.map({$0.toCamelCase})
newLayer["layout"] = layout
}

return newLayer
}
styleJson["layers"] = layers
Expand Down Expand Up @@ -151,6 +190,71 @@ internal class StylesController {
}
return request.fileio.writeFile(byteBuffer, at: stylesFile)
}

private func analyseUsage(request: Request, id: String) -> EventLoopFuture<(fonts: [String], icons: [String])> {
return request.application.fileio.openFile(
path: "\(folder)/\(id).json",
mode: .read,
eventLoop: request.eventLoop
).flatMap { fileHandle in
return request.application.fileio.read(fileHandle: fileHandle, byteCount: 131_072, allocator: .init(), eventLoop: request.eventLoop).flatMapThrowing { buffer in
guard let styleJson = try? JSONSerialization.jsonObject(with: Data(buffer: buffer)) as? [String: Any],
let layers = styleJson["layers"] as? [[String: Any]]
else {
throw Abort(.badRequest, reason: "style.json is not valid josn")
}
var fonts = Set<String>()
var icons = Set<String>()

// TODO: Implement Resolved Icons
for layer in layers {
if let layout = layer["layout"] as? [String: Any] {
if let textFonts = layout["text-font"] as? [String], !textFonts.isEmpty {
fonts.insert(textFonts[0])
}
if let iconImage = layout["icon-image"] as? String, !iconImage.isEmpty {
icons.insert(iconImage)
}
}
if let paint = layer["paint"] as? [String: Any] {
if let backgroundPattern = paint["background-pattern"] as? String, !backgroundPattern.isEmpty {
icons.insert(backgroundPattern)
}
if let fillPattern = paint["fill-pattern"] as? String, !fillPattern.isEmpty {
icons.insert(fillPattern)
}
if let linePattern = paint["line-pattern"] as? String, !linePattern.isEmpty {
icons.insert(linePattern)
}
if let fillExtrusionPattern = paint["fill-extrusion-pattern"] as? String, !fillExtrusionPattern.isEmpty {
icons.insert(fillExtrusionPattern)
}
}

}
return (fonts: Array(fonts), icons: Array(icons))
}.always { _ in
try? fileHandle.close()
}
}
}

private func analyseAvilableIcons(request: Request, id: String) -> EventLoopFuture<([String])> {
return request.application.fileio.openFile(
path: "\(folder)/\(id)/sprite.json",
mode: .read,
eventLoop: request.eventLoop
).flatMap { fileHandle in
return request.application.fileio.read(fileHandle: fileHandle, byteCount: 131_072, allocator: .init(), eventLoop: request.eventLoop).flatMapThrowing { buffer in
guard let iconsJson = try? JSONSerialization.jsonObject(with: Data(buffer: buffer)) as? [String: Any] else {
throw Abort(.badRequest, reason: "sprite.json is not valid josn")
}
return Array(iconsJson.keys)
}.always { _ in
try? fileHandle.close()
}
}
}


}
1 change: 1 addition & 0 deletions Sources/SwiftTileserverCache/Misc/String+BashEncode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ internal extension String {
.replacingOccurrences(of: "#", with: "\\#")
.replacingOccurrences(of: "(", with: "\\(")
.replacingOccurrences(of: ")", with: "\\)")
.replacingOccurrences(of: " ", with: "\\ ")
}
}
20 changes: 20 additions & 0 deletions Sources/SwiftTileserverCache/Misc/String+CamelCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// String+CamelCase.swift
// SwiftTileserverCache
//
// Created by Florian Kostenzer on 11.02.21.
//

import Foundation

internal extension String {
var toCamelCase: String {
return self.components(separatedBy: ["_", "-", " ", "."])
.map({
$0.replacingCharacters(
in: ...$0.startIndex,
with: $0.first?.uppercased() ?? ""
)
}).joined()
}
}
14 changes: 14 additions & 0 deletions Sources/SwiftTileserverCache/Misc/String+IsEmpty.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// String+IsEmpty.swift
// SwiftTileserverCache
//
// Created by Florian Kostenzer on 11.02.21.
//

import Foundation

internal extension String {
var isEmpty: Bool {
return self.trimmingCharacters(in: .whitespaces) == ""
}
}
7 changes: 7 additions & 0 deletions Sources/SwiftTileserverCache/Model/Style.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@
import Vapor

public struct Style: Content {

public struct Analysis: Codable {
var missingFonts: [String]
var missingIcons: [String]
}

public var id: String
public var name: String
public var external: Bool?
public var url: String?
public var analysis: Analysis?

public var removingURL: Style {
return Style(id: id, name: name, external: external, url: nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal class StylesViewController: ViewController {
}

internal func render(request: Request) throws -> EventLoopFuture<View> {
return stylesController.get(request: request).flatMap { (styles) in
return stylesController.getWithAnalysis(request: request).flatMap { (styles) in
let context = Context(
pageId: "styles",
pageName: "Styles",
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftTileserverCache/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ func routes(_ app: Application) throws {
app.routes.defaultMaxBodySize = ByteCount(stringLiteral: maxBodySize)
}

let stylesController = StylesController(tileServerURL: tileServerURL, externalStyles: externalStyles, folder: "TileServer/Styles")
let fontsController = FontsController(folder: "TileServer/Fonts", tempFolder: "Temp")
let stylesController = StylesController(tileServerURL: tileServerURL, externalStyles: externalStyles, folder: "TileServer/Styles", fontsController: fontsController)
app.get("styles", use: stylesController.get)

let tileController = TileController(tileServerURL: tileServerURL, statsController: statsController, stylesController: stylesController)
Expand Down Expand Up @@ -53,7 +54,6 @@ func routes(_ app: Application) throws {
protected.webSocket("api", "datasets", "add", onUpgrade: datasetController.download)
protected.webSocket("api", "datasets", "delete", onUpgrade: datasetController.delete)

let fontsController = FontsController(folder: "TileServer/Fonts", tempFolder: "Temp")
protected.on(.POST, "api", "fonts", "add", body: .collect(maxSize: "64mb"), use: fontsController.add)
protected.delete("api", "fonts", "delete", ":name", use: fontsController.delete)

Expand Down

0 comments on commit a97c72b

Please sign in to comment.