diff --git a/icons/poi_book_upright.svg b/icons/poi_book_upright.svg new file mode 100644 index 000000000..b13bdc7bc --- /dev/null +++ b/icons/poi_book_upright.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/poi_car_repair.svg b/icons/poi_car_repair.svg new file mode 100644 index 000000000..042bff803 --- /dev/null +++ b/icons/poi_car_repair.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/poi_car_shop.svg b/icons/poi_car_shop.svg new file mode 100644 index 000000000..cfcf82ba7 --- /dev/null +++ b/icons/poi_car_shop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/poi_hostel.svg b/icons/poi_hostel.svg new file mode 100644 index 000000000..04d8bf388 --- /dev/null +++ b/icons/poi_hostel.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/poi_hotel.svg b/icons/poi_hotel.svg new file mode 100644 index 000000000..576f5a1d7 --- /dev/null +++ b/icons/poi_hotel.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/poi_taxi.svg b/icons/poi_taxi.svg new file mode 100644 index 000000000..c8dcb2193 --- /dev/null +++ b/icons/poi_taxi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/shield_us_az_scenic.svg b/icons/shield_us_az_scenic.svg new file mode 100644 index 000000000..bff968fbd --- /dev/null +++ b/icons/shield_us_az_scenic.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/scripts/taginfo_template.json b/scripts/taginfo_template.json index 868967ced..32399044d 100644 --- a/scripts/taginfo_template.json +++ b/scripts/taginfo_template.json @@ -399,6 +399,14 @@ "doc_url": "https://openmaptiles.org/schema/#poi", "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_school.svg" }, + { + "key": "amenity", + "value": "taxi", + "object_types": ["node", "area"], + "description": "Taxi stands are marked by an icon representing the front view of a taxi cab.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_taxi.svg" + }, { "key": "amenity", "value": "kindergarten", @@ -479,6 +487,38 @@ "doc_url": "https://openmaptiles.org/schema/#poi", "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_town_hall.svg" }, + { + "key": "tourism", + "value": "guest_house", + "object_types": ["node", "area"], + "description": "Guest houses are marked by an icon representing a person laying in a bed.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hotel.svg" + }, + { + "key": "tourism", + "value": "hostel", + "object_types": ["node", "area"], + "description": "Hostels are marked by an icon representing two people laying in a bunk bed.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hostel.svg" + }, + { + "key": "tourism", + "value": "hotel", + "object_types": ["node", "area"], + "description": "Hotels are marked by an icon representing a person laying in a bed.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hotel.svg" + }, + { + "key": "tourism", + "value": "motel", + "object_types": ["node", "area"], + "description": "Motels are marked by an icon representing a person laying in a bed.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hotel.svg" + }, { "key": "tourism", "value": "museum", @@ -487,7 +527,6 @@ "doc_url": "https://openmaptiles.org/schema/#poi", "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_museum.svg" }, - { "key": "amenity", "value": "clinic", @@ -496,6 +535,14 @@ "doc_url": "https://openmaptiles.org/schema/#poi", "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_health_cross.svg" }, + { + "key": "amenity", + "value": "library", + "object_types": ["node", "area"], + "description": "Libraries are marked by an icon representing a book.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_book_upright.svg" + }, { "key": "amenity", "value": "parking", @@ -632,6 +679,22 @@ "doc_url": "https://openmaptiles.org/schema/#poi", "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_pow_taoist.svg" }, + { + "key": "shop", + "value": "car", + "object_types": ["node", "area"], + "description": "Car dealerships are marked by an icon representing a front facing vehicle.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_car_shop.svg" + }, + { + "key": "shop", + "value": "car_repair", + "object_types": ["node", "area"], + "description": "Car mechanic shops are marked by an icon representing a front facing vehicle with a wrench above it.", + "doc_url": "https://openmaptiles.org/schema/#poi", + "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_car_repair.svg" + }, { "key": "shop", "value": "supermarket", diff --git a/shieldlib/src/screen_gfx.ts b/shieldlib/src/screen_gfx.ts index eec3087e8..3be40e4be 100644 --- a/shieldlib/src/screen_gfx.ts +++ b/shieldlib/src/screen_gfx.ts @@ -5,7 +5,9 @@ import rgba from "color-rgba"; const defaultFontFamily = '"sans-serif-condensed", "Arial Narrow", sans-serif'; export const shieldFont = (size: number, fontFamily: string) => `condensed 500 ${size}px ${fontFamily || defaultFontFamily}`; -export const fontSizeThreshold = 12; + +//If a computed shield font size is below this value, choose a wider shield if possible +export const fontSizeThreshold = 11.8; // Replaces `sourceVal` with a blend of `lightenVal` and `darkenVal` proportional to the brightness; // i.e. white becomes `darkenVal`, black becomes `lightenVal`, and anit-aliased pixels remain anit-aliased diff --git a/shieldlib/src/shield.js b/shieldlib/src/shield.js index f8d3c342d..04ff352f4 100644 --- a/shieldlib/src/shield.js +++ b/shieldlib/src/shield.js @@ -76,18 +76,6 @@ function getDrawFunc(shieldDef) { return ShieldDraw.blank; } -function drawShield(r, ctx, shieldDef, routeDef) { - let bannerCount = getBannerCount(shieldDef); - let yOffset = bannerCount * r.px(r.options.bannerHeight); - - //Shift canvas to draw shield below banner - ctx.save(); - ctx.translate(0, yOffset); - let drawFunc = getDrawFunc(shieldDef); - drawFunc(r, ctx, routeDef.ref); - ctx.restore(); -} - function getDrawHeight(r, shieldDef) { if (typeof shieldDef.shapeBlank != "undefined") { return ShieldDraw.shapeHeight(r, shieldDef.shapeBlank.drawFunc); @@ -95,30 +83,7 @@ function getDrawHeight(r, shieldDef) { return r.shieldSize(); } -function drawShieldText(r, ctx, shieldDef, routeDef) { - var bannerCount = getBannerCount(shieldDef); - var shieldBounds = null; - - var shieldArtwork = getRasterShieldBlank(r, shieldDef, routeDef); - let yOffset = bannerCount * r.px(r.options.bannerHeight); - - if (shieldArtwork == null) { - ctx.translate(0, yOffset); - let drawFunc = getDrawFunc(shieldDef); - drawFunc(r, ctx, routeDef.ref); - ctx.translate(0, -yOffset); - - shieldBounds = { - width: ctx.canvas.width, - height: getDrawHeight(r, shieldDef), - }; - } else { - shieldBounds = { - width: shieldArtwork.data.width, - height: shieldArtwork.data.height, - }; - } - +function drawShieldText(r, ctx, shieldDef, routeDef, shieldBounds) { if (shieldDef.notext) { //If the shield definition says not to draw a ref, ignore ref return ctx; @@ -132,8 +97,6 @@ function drawShieldText(r, ctx, shieldDef, routeDef) { shieldBounds ); - textLayout.yBaseline += bannerCount * r.px(r.options.bannerHeight); - if (typeof r.options.SHIELD_TEXT_HALO_COLOR_OVERRIDE !== "undefined") { ctx.strokeStyle = options.SHIELD_TEXT_HALO_COLOR_OVERRIDE; ShieldText.drawShieldHaloText(r, ctx, routeDef.ref, textLayout); @@ -150,8 +113,7 @@ function drawShieldText(r, ctx, shieldDef, routeDef) { ctx.lineWidth = r.px(1); ctx.strokeRect( r.px(shieldDef.padding.left - 0.5), - bannerCount * r.px(r.options.bannerHeight) + - r.px(shieldDef.padding.top - 0.5), + r.px(shieldDef.padding.top - 0.5), shieldBounds.width - r.px(shieldDef.padding.left + shieldDef.padding.right - 1), shieldBounds.height - @@ -294,6 +256,17 @@ function getDrawnShieldBounds(r, shieldDef, ref) { return { width, height }; } +function bannerAreaHeight(r, bannerCount) { + if (bannerCount === 0) { + return 0; + } + return ( + bannerCount * r.px(r.options.bannerHeight) + + //No padding after last banner + (bannerCount - 1) * r.px(r.options.bannerPadding) + ); +} + export function generateShieldCtx(r, routeDef) { let shieldDef = getShieldDef(r.shieldDef, routeDef); @@ -311,18 +284,25 @@ export function generateShieldCtx(r, routeDef) { let width = r.shieldSize(); let height = r.shieldSize(); + let shieldBounds = null; + if (sourceSprite == null) { if (typeof shieldDef.shapeBlank != "undefined") { let bounds = getDrawnShieldBounds(r, shieldDef, routeDef.ref); width = bounds.width; height = bounds.height; } + shieldBounds = { + width: width, + height: getDrawHeight(r, shieldDef), + }; } else { width = sourceSprite.data.width; height = sourceSprite.data.height; + shieldBounds = { width, height }; } - let bannerHeight = bannerCount * r.px(r.options.bannerHeight); + let bannerHeight = bannerAreaHeight(r, bannerCount); height += bannerHeight; //Generate empty canvas sized to the graphic @@ -338,9 +318,15 @@ export function generateShieldCtx(r, routeDef) { // Add the halo around modifier plaque text drawBannerHalos(r, ctx, shieldDef); + //Shift canvas to draw shield below banner + ctx.save(); + ctx.translate(0, bannerHeight); + if (sourceSprite == null) { - drawShield(r, ctx, shieldDef, routeDef); + let drawFunc = getDrawFunc(shieldDef); + drawFunc(r, ctx, routeDef.ref); } else { + //This is a raw copy, so the yOffset (bannerHeight) is needed Gfx.transposeImageData( ctx, sourceSprite, @@ -352,7 +338,9 @@ export function generateShieldCtx(r, routeDef) { } // Draw the shield text - drawShieldText(r, ctx, shieldDef, routeDef); + drawShieldText(r, ctx, shieldDef, routeDef, shieldBounds); + + ctx.restore(); // Add modifier plaque text drawBanners(r, ctx, shieldDef); diff --git a/shieldlib/src/shield_banner.ts b/shieldlib/src/shield_banner.ts index 6961865b5..deb86599d 100644 --- a/shieldlib/src/shield_banner.ts +++ b/shieldlib/src/shield_banner.ts @@ -133,7 +133,7 @@ function drawBannerTextComponent( textComponent: boolean ): void { const bannerPadding = { - top: r.options.bannerPadding, + top: 0, bottom: 0, left: 0, right: 0, @@ -161,7 +161,7 @@ function drawBannerTextComponent( text, textLayout.xBaseline, textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + bannerIndex * r.px(r.options.bannerHeight + r.options.bannerPadding) ); } else { ctx.shadowBlur = 0; @@ -170,7 +170,7 @@ function drawBannerTextComponent( text, textLayout.xBaseline, textLayout.yBaseline + - bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + bannerIndex * r.px(r.options.bannerHeight + r.options.bannerPadding) ); ctx.shadowColor = null; diff --git a/shieldlib/src/shield_text.ts b/shieldlib/src/shield_text.ts index 350f7f02c..5bc1071d0 100644 --- a/shieldlib/src/shield_text.ts +++ b/shieldlib/src/shield_text.ts @@ -137,6 +137,15 @@ function triangleDownTextConstraint( }; } +// Warning!!! Hack!!! +function isRunningInWebKit(): boolean { + if (typeof window === "undefined") { + return false; + } + const userAgent = window.navigator.userAgent; + return /WebKit/i.test(userAgent) && !/Chrome/i.test(userAgent); +} + /** * Determines the position and font size to draw text so that it fits within * a bounding box. @@ -157,34 +166,49 @@ export function layoutShieldText( textLayoutDef: TextLayout, maxFontSize: number = 14 ): TextPlacement { - var padTop = r.px(padding.top) || 0; - var padBot = r.px(padding.bottom) || 0; - var padLeft = r.px(padding.left) || 0; - var padRight = r.px(padding.right) || 0; + let padTop = r.px(padding.top) || 0; + let padBot = r.px(padding.bottom) || 0; + let padLeft = r.px(padding.left) || 0; + let padRight = r.px(padding.right) || 0; - var maxFont = r.px(maxFontSize); + let maxFont = r.px(maxFontSize); //Temporary canvas for text measurment - var ctx = r.gfxFactory.createGraphics(bounds); + let ctx: CanvasRenderingContext2D = r.gfxFactory.createGraphics(bounds); ctx.font = Gfx.shieldFont(Gfx.fontSizeThreshold, r.options.shieldFont); - ctx.textAlign = "center"; + ctx.textAlign = "left"; ctx.textBaseline = "top"; - var metrics = ctx.measureText(text); + let metrics: TextMetrics = ctx.measureText(text); - var textWidth = metrics.width; - var textHeight = metrics.actualBoundingBoxDescent; + let textWidth: number = + Math.abs(metrics.actualBoundingBoxLeft) + + Math.abs(metrics.actualBoundingBoxRight); + let textHeight: number = + Math.abs(metrics.actualBoundingBoxDescent) + + Math.abs(metrics.actualBoundingBoxAscent); - var availHeight = bounds.height - padTop - padBot; - var availWidth = bounds.width - padLeft - padRight; + //Adjust for excess descender text height across browsers + textHeight *= 0.9; - var xBaseline = padLeft + availWidth / 2; + //Adjust for excess text height measured in Webkit engine specifically + if (isRunningInWebKit()) { + textHeight *= 0.54; + } + + let availHeight: number = bounds.height - padTop - padBot; + let availWidth: number = bounds.width - padLeft - padRight; + + let xBaseline: number = padLeft + availWidth / 2; let textLayoutFunc = drawTextFunctions[textLayoutDef.constraintFunc]; - let textConstraint = textLayoutFunc( - { height: availHeight, width: availWidth }, - { height: textHeight, width: textWidth }, + let spaceAvail: Dimension = { height: availHeight, width: availWidth }; + let measuredTextBounds: Dimension = { height: textHeight, width: textWidth }; + + let textConstraint: TextTransform = textLayoutFunc( + spaceAvail, + measuredTextBounds, textLayoutDef.options ); @@ -195,20 +219,23 @@ export function layoutShieldText( ); ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); - ctx.textAlign = "center"; + ctx.textAlign = "left"; ctx.textBaseline = "top"; metrics = ctx.measureText(text); - textHeight = metrics.actualBoundingBoxDescent; + textHeight = + Math.abs(metrics.actualBoundingBoxDescent) + + Math.abs(metrics.actualBoundingBoxAscent); let yBaseline: number; switch (textConstraint.valign) { case VerticalAlignment.Top: - yBaseline = padTop; + yBaseline = padTop + metrics.actualBoundingBoxAscent; break; case VerticalAlignment.Bottom: - yBaseline = padTop + availHeight - textHeight; + yBaseline = + padTop + availHeight - textHeight + metrics.actualBoundingBoxAscent; break; case VerticalAlignment.Middle: default: diff --git a/src/js/shield_defs.js b/src/js/shield_defs.js index a906aeb39..04bfd834b 100644 --- a/src/js/shield_defs.js +++ b/src/js/shield_defs.js @@ -653,6 +653,12 @@ export function loadShields() { )) ); + // Arizona + shields["US:AZ:Scenic"] = { + spriteBlank: "shield_us_az_scenic", + notext: true, + }; + // Arkansas shields["US:AR"] = { spriteBlank: ["shield_us_ar_2", "shield_us_ar_3"], @@ -3320,6 +3326,7 @@ export function loadShields() { shields["omt-ie-national"] = roundedRectShield( Color.shields.green, + Color.shields.white, Color.shields.yellow ); @@ -3535,6 +3542,7 @@ export function loadShields() { shields["omt-gb-trunk"] = roundedRectShield( Color.shields.green, + Color.shields.white, Color.shields.yellow ); diff --git a/src/layer/poi.js b/src/layer/poi.js index c2447533d..cd9c3d1ed 100644 --- a/src/layer/poi.js +++ b/src/layer/poi.js @@ -35,6 +35,30 @@ var iconDefs = { color: Color.poi.transport, description: "Bus stop", }, + car_repair: { + classes: { + car: ["car_repair"], + }, + sprite: "poi_car_repair", + color: Color.poi.consumer, + description: "Car mechanic", + }, + car_shop: { + classes: { + car: ["car"], + }, + sprite: "poi_car_shop", + color: Color.poi.consumer, + description: "Car dealership", + }, + taxi: { + classes: { + office: ["taxi"], + }, + sprite: "poi_taxi", + color: Color.poi.transport, + description: "Taxi stand", + }, coffee: { classes: { cafe: ["cafe"], @@ -59,6 +83,30 @@ var iconDefs = { color: Color.poi.infrastructure, description: "Hospital", }, + hotel: { + classes: { + lodging: ["hotel", "motel", "guest_house"], + }, + sprite: "poi_hotel", + color: Color.poi.consumer, + description: "Hotel", + }, + hostel: { + classes: { + lodging: ["hostel"], + }, + sprite: "poi_hostel", + color: Color.poi.consumer, + description: "Hostel", + }, + library: { + classes: { + library: ["library"], + }, + sprite: "poi_book_upright", + color: Color.poi.infrastructure, + description: "Library", + }, medical: { classes: { hospital: ["clinic"], @@ -259,6 +307,10 @@ export const poi = { ...getSubclasses(iconDefs.bar), ...getSubclasses(iconDefs.coffee), ...getSubclasses(iconDefs.supermarket), + ...getSubclasses(iconDefs.car_shop), + ...getSubclasses(iconDefs.car_repair), + ...getSubclasses(iconDefs.hotel), + ...getSubclasses(iconDefs.hostel), ], Color.poi.consumer, [ @@ -266,6 +318,7 @@ export const poi = { "bus_stop", ...getSubclasses(iconDefs.railway_station), ...getSubclasses(iconDefs.railway_stop), + ...getSubclasses(iconDefs.taxi), ], Color.poi.transport, ["museum"], @@ -276,6 +329,7 @@ export const poi = { "police", "school", "college", + "library", "townhall", ...getSubclasses(iconDefs.pow_christian), ...getSubclasses(iconDefs.pow_buddhist), @@ -303,6 +357,7 @@ export const poi = { [ "bus_stop", "hospital", + "library", "museum", "police", ...getSubclasses(iconDefs.fuel), @@ -320,7 +375,15 @@ export const poi = { "tram_stop", ], 15, - [...getSubclasses(iconDefs.bar), ...getSubclasses(iconDefs.coffee)], + [ + ...getSubclasses(iconDefs.bar), + ...getSubclasses(iconDefs.coffee), + ...getSubclasses(iconDefs.car_shop), + ...getSubclasses(iconDefs.car_repair), + ...getSubclasses(iconDefs.taxi), + ...getSubclasses(iconDefs.hotel), + ...getSubclasses(iconDefs.hostel), + ], 16, ["clinic", "doctors", "parking"], 17, diff --git a/test/sample_locations.json b/test/sample_locations.json index 2ff11454f..8a150106d 100644 --- a/test/sample_locations.json +++ b/test/sample_locations.json @@ -48,6 +48,14 @@ "height": 400 } }, + { + "location": "17/40.753326/-73.982224", + "name": "library_z17", + "viewport": { + "width": 400, + "height": 400 + } + }, { "location": "13.25/49.552/5.8107", "name": "europe_e-road_routes", @@ -55,5 +63,29 @@ "width": 400, "height": 400 } + }, + { + "location": "16/38.896954/-77.108406", + "name": "virginia_", + "viewport": { + "width": 400, + "height": 400 + } + }, + { + "location": "16/38.906341/-77.025889", + "name": "dc_logan_circle", + "viewport": { + "width": 400, + "height": 400 + } + }, + { + "location": "16/40.755264/-73.987405", + "name": "manhattan_times_square", + "viewport": { + "width": 400, + "height": 400 + } } ]