diff --git a/src/place_building_blocks/place_photo_gallery/place_photo_gallery.ts b/src/place_building_blocks/place_photo_gallery/place_photo_gallery.ts index 2605c47..87b89fb 100644 --- a/src/place_building_blocks/place_photo_gallery/place_photo_gallery.ts +++ b/src/place_building_blocks/place_photo_gallery/place_photo_gallery.ts @@ -42,6 +42,16 @@ interface TileSize { heightPx: number; } +/** + * Returns the desired photo size in pixels based on CSS pixels and max size, + * accounting for diff between physical and CSS pixels on current device; see: + * https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio. + */ +function getPhotoSize(cssPx: number, max: number) { + const devicePx = Math.ceil(cssPx * window.devicePixelRatio); + return Math.min(devicePx, max); +} + /** * Formats a `google.maps.places.Photo` object for display based on tile size. * @@ -60,23 +70,19 @@ interface TileSize { */ function formatPhoto(photo: Photo, tileSize: TileSize): FormattedPhoto { const photoSizeRatio = photo.widthPx / photo.heightPx; + const windowSizeRatio = window.innerWidth / window.innerHeight; const tileSizeRatio = tileSize.widthPx / tileSize.heightPx; - // Account for diff between physical and CSS pixels on current device; see: - // https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio. + const lightboxPhotoOptions = photoSizeRatio > windowSizeRatio ? + {maxHeight: getPhotoSize(window.innerHeight, MAX_PHOTO_SIZE_PX)} : + {maxWidth: getPhotoSize(window.innerWidth, MAX_PHOTO_SIZE_PX)}; const tilePhotoOptions = photoSizeRatio > tileSizeRatio ? - {maxHeight: Math.ceil(tileSize.heightPx * window.devicePixelRatio)} : - {maxWidth: Math.ceil(tileSize.widthPx * window.devicePixelRatio)}; + {maxHeight: getPhotoSize(tileSize.heightPx, MAX_TILE_PHOTO_SIZE_PX)} : + {maxWidth: getPhotoSize(tileSize.widthPx, MAX_TILE_PHOTO_SIZE_PX)}; return { - uri: photo.getURI({ - maxHeight: MAX_PHOTO_SIZE_PX, - maxWidth: MAX_PHOTO_SIZE_PX, - }), - tileUri: photo.getURI({ - maxHeight: tilePhotoOptions.maxHeight || MAX_TILE_PHOTO_SIZE_PX, - maxWidth: tilePhotoOptions.maxWidth || MAX_TILE_PHOTO_SIZE_PX, - }), + uri: photo.getURI(lightboxPhotoOptions), + tileUri: photo.getURI(tilePhotoOptions), attributions: photo.authorAttributions, }; } @@ -358,9 +364,13 @@ export class PlacePhotoGallery extends PlaceDataConsumer { protected override updated() { if (!this.tileSize && this.firstTileElement) { + // Note that sometimes a tile's BoundingClientRect becomes defined outside + // Lit's reactive update cycle. In such cases the tile size will be zero, + // and we cap width/height at `MAX_TILE_PHOTO_SIZE_PX` to avoid requesting + // an overly large image. this.tileSize = { - widthPx: this.firstTileElement.clientWidth, - heightPx: this.firstTileElement.clientHeight, + widthPx: this.firstTileElement.clientWidth || MAX_TILE_PHOTO_SIZE_PX, + heightPx: this.firstTileElement.clientHeight || MAX_TILE_PHOTO_SIZE_PX, }; } } diff --git a/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts b/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts index b2ab15a..3a7603c 100644 --- a/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts +++ b/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts @@ -16,7 +16,6 @@ import type {Place, PlaceResult} from '../../utils/googlemaps_types.js'; import {PlacePhotoGallery} from './place_photo_gallery.js'; - const fakePlace = makeFakePlace({ id: '1234567890', displayName: 'Place Name', @@ -103,12 +102,14 @@ describe('PlacePhotoGallery', () => { maxTiles?: number, place?: Place|PlaceResult, clickOnTile?: number, + hidden?: boolean, }) { const root = env.render(html` `); @@ -161,10 +162,12 @@ describe('PlacePhotoGallery', () => { const {tiles} = await prepareState({place: fakePlace}); expect(tiles.length).toBe(3); - expect(getUri0Spy).toHaveBeenCalledWith({maxHeight: 4800, maxWidth: 4800}); - expect(getUri0Spy).toHaveBeenCalledWith({maxHeight: 134, maxWidth: 1200}); - expect(getUri1Spy).toHaveBeenCalledWith({maxHeight: 1200, maxWidth: 142}); - expect(getUri2Spy).toHaveBeenCalledWith({maxHeight: 1200, maxWidth: 142}); + expect(getUri0Spy).toHaveBeenCalledWith({maxHeight: window.innerHeight}); + expect(getUri0Spy).toHaveBeenCalledWith({maxHeight: 134}); + expect(getUri1Spy).toHaveBeenCalledWith({maxWidth: window.innerWidth}); + expect(getUri1Spy).toHaveBeenCalledWith({maxWidth: 142}); + expect(getUri2Spy).toHaveBeenCalledWith({maxWidth: window.innerWidth}); + expect(getUri2Spy).toHaveBeenCalledWith({maxWidth: 142}); expect(getComputedStyle(tiles[0]).backgroundImage) .toBe('url("https://lh3.googleusercontent.com/places/A")'); expect(getComputedStyle(tiles[1]).backgroundImage) @@ -173,6 +176,26 @@ describe('PlacePhotoGallery', () => { .toBe('url("https://lh3.googleusercontent.com/places/C")'); }); + it('accounts for device pixel ratio when generating image URIs', async () => { + spyOnProperty(window, 'devicePixelRatio').and.returnValue(2); + const getUriSpy = spyOn(fakePlace.photos![0], 'getURI').and.callThrough(); + + await prepareState({place: fakePlace}); + + expect(getUriSpy).toHaveBeenCalledWith({maxHeight: window.innerHeight * 2}); + expect(getUriSpy).toHaveBeenCalledWith({maxHeight: 134 * 2}); + }); + + it('gets images with max tile size when tile has unknown size', async () => { + const getUri0Spy = spyOn(fakePlace.photos![0], 'getURI').and.callThrough(); + const getUri1Spy = spyOn(fakePlace.photos![1], 'getURI').and.callThrough(); + + await prepareState({place: fakePlace, hidden: true}); + + expect(getUri0Spy).toHaveBeenCalledWith({maxHeight: 1200}); + expect(getUri1Spy).toHaveBeenCalledWith({maxWidth: 1200}); + }); + it('shows all photos from PlaceResult data as tiles, in order', async () => { const getUrl0Spy = spyOn(fakePlaceResult.photos![0], 'getUrl').and.callThrough(); @@ -184,10 +207,12 @@ describe('PlacePhotoGallery', () => { const {tiles} = await prepareState({place: fakePlaceResult}); expect(tiles.length).toBe(3); - expect(getUrl0Spy).toHaveBeenCalledWith({maxHeight: 4800, maxWidth: 4800}); - expect(getUrl0Spy).toHaveBeenCalledWith({maxHeight: 134, maxWidth: 1200}); - expect(getUrl1Spy).toHaveBeenCalledWith({maxHeight: 1200, maxWidth: 142}); - expect(getUrl2Spy).toHaveBeenCalledWith({maxHeight: 1200, maxWidth: 142}); + expect(getUrl0Spy).toHaveBeenCalledWith({maxHeight: window.innerHeight}); + expect(getUrl0Spy).toHaveBeenCalledWith({maxHeight: 134}); + expect(getUrl1Spy).toHaveBeenCalledWith({maxWidth: window.innerWidth}); + expect(getUrl1Spy).toHaveBeenCalledWith({maxWidth: 142}); + expect(getUrl2Spy).toHaveBeenCalledWith({maxWidth: window.innerWidth}); + expect(getUrl2Spy).toHaveBeenCalledWith({maxWidth: 142}); expect(getComputedStyle(tiles[0]).backgroundImage) .toBe('url("https://lh3.googleusercontent.com/places/A")'); expect(getComputedStyle(tiles[1]).backgroundImage)