diff --git a/src/scripts/components/app/app.styl b/src/scripts/components/app/app.styl index b1b1e3cd1..2fd56d683 100644 --- a/src/scripts/components/app/app.styl +++ b/src/scripts/components/app/app.styl @@ -5,6 +5,11 @@ font-family: NotesEsa src: url('../../../../assets/fonts/NotesEsaReg.otf') +@font-face + font-style: normal + font-family: NotesEsaBold + src: url('../../../../assets/fonts/NotesEsaBol.otf') + :global(html), :global(body), :global(#app) overflow: hidden margin: 0 diff --git a/src/scripts/components/app/app.tsx b/src/scripts/components/app/app.tsx index d61dd5edb..59051f63e 100644 --- a/src/scripts/components/app/app.tsx +++ b/src/scripts/components/app/app.tsx @@ -24,6 +24,7 @@ import Globes from '../globes/globes'; import translations from '../../i18n'; import styles from './app.styl'; +import {useStoryMarkers} from '../../hooks/use-story-markers'; // create redux store const store = createReduxStore(); @@ -36,6 +37,7 @@ const App: FunctionComponent = () => ( const TranslatedApp: FunctionComponent = () => { const language = useSelector(languageSelector); + const markers = useStoryMarkers(); return ( @@ -58,7 +60,7 @@ const TranslatedApp: FunctionComponent = () => {
- + diff --git a/src/scripts/components/globe/globe.tsx b/src/scripts/components/globe/globe.tsx index f6191aff1..807d4659c 100644 --- a/src/scripts/components/globe/globe.tsx +++ b/src/scripts/components/globe/globe.tsx @@ -21,9 +21,11 @@ import {isElectron} from '../../libs/electron/index'; import {GlobeView} from '../../types/globe-view'; import {GlobeProjection} from '../../types/globe-projection'; import config from '../../config/main'; +import {useMarkers} from '../../hooks/use-markers'; import {GlobeProjectionState} from '../../types/globe-projection-state'; import {BasemapId} from '../../types/basemap'; +import {Marker} from '../../types/marker-type'; import styles from './globe.styl'; @@ -52,6 +54,7 @@ interface Props { basemap: BasemapId | null; zoomLevels: number; flyTo: GlobeView | null; + markers?: Marker[]; onMouseEnter: () => void; onTouchStart: () => void; onChange: (view: GlobeView) => void; @@ -79,6 +82,7 @@ const Globe: FunctionComponent = ({ zoomLevels, active, flyTo, + markers = [], onMouseEnter, onTouchStart, onChange, @@ -295,6 +299,8 @@ const Globe: FunctionComponent = ({ flyToGlobeView(viewer, flyTo); }, [viewer, flyTo]); + useMarkers(viewer, markers); + return (
{ +interface Props { + markers?: Marker[]; +} + +const Globes: FunctionComponent = ({markers = []}) => { const dispatch = useDispatch(); const selectedLayerIds = useSelector(selectedLayerIdsSelector); const projectionState = useSelector(projectionSelector); @@ -84,6 +89,7 @@ const Globes: FunctionComponent = () => { /> )} {
{selectedMainLayer && ( diff --git a/src/scripts/components/navigation/navigation.tsx b/src/scripts/components/navigation/navigation.tsx index cfdc3e72b..370501f30 100644 --- a/src/scripts/components/navigation/navigation.tsx +++ b/src/scripts/components/navigation/navigation.tsx @@ -13,8 +13,8 @@ import {MenuIcon} from '../icons/menu-icon'; import styles from './navigation.styl'; const Navigation: FunctionComponent = () => { - const [showMenu, setShowMenu] = useState(false); const dispatch = useDispatch(); + const [showMenu, setShowMenu] = useState(false); return (
diff --git a/src/scripts/components/splash-screen/splash-screen.styl b/src/scripts/components/splash-screen/splash-screen.styl index 59cd1736b..b573b4344 100644 --- a/src/scripts/components/splash-screen/splash-screen.styl +++ b/src/scripts/components/splash-screen/splash-screen.styl @@ -21,7 +21,7 @@ text-transform: uppercase font-weight: bold font-size: emCalc(36px) - font-family: NotesEsa + font-family: NotesEsaBold line-height: emCalc(16px) p diff --git a/src/scripts/custom.d.ts b/src/scripts/custom.d.ts index 4c18f08d7..08dfe991a 100644 --- a/src/scripts/custom.d.ts +++ b/src/scripts/custom.d.ts @@ -2,3 +2,13 @@ declare module '*.styl' { const content: any; export default content; } + +declare module '*.svg' { + const content: string; + export default content; +} + +declare module '*.otf' { + const content: string; + export default content; +} diff --git a/src/scripts/hooks/use-markers.ts b/src/scripts/hooks/use-markers.ts new file mode 100644 index 000000000..e25beaea7 --- /dev/null +++ b/src/scripts/hooks/use-markers.ts @@ -0,0 +1,47 @@ +import {useEffect} from 'react'; +import {useDispatch} from 'react-redux'; +import {useHistory} from 'react-router-dom'; +import { + Viewer, + ScreenSpaceEventHandler, + defined, + ScreenSpaceEventType +} from 'cesium'; + +import {createMarker} from '../libs/create-marker'; + +import {Marker} from '../types/marker-type'; + +export const useMarkers = (viewer: Viewer | null, markers: Marker[]) => { + const history = useHistory(); + const dispatch = useDispatch(); + + // create marker for each story + useEffect(() => { + if (!viewer) { + return; + } + + const scene = viewer.scene; + const handler = new ScreenSpaceEventHandler( + scene.canvas as HTMLCanvasElement + ); + + handler.setInputAction(movement => { + const pickedObject = scene.pick(movement.position); + if (defined(pickedObject)) { + history.push(`/stories/${pickedObject.id._id}/0`); + } + }, ScreenSpaceEventType.LEFT_CLICK); + + Promise.all(markers.map(marker => createMarker(marker))).then(entities => { + viewer.entities.removeAll(); + entities.forEach(entity => viewer.entities.add(entity)); + }); + + // eslint-disable-next-line consistent-return + return () => handler.destroy(); + }, [dispatch, history, markers, viewer]); + + return; +}; diff --git a/src/scripts/hooks/use-story-markers.ts b/src/scripts/hooks/use-story-markers.ts new file mode 100644 index 000000000..517107f91 --- /dev/null +++ b/src/scripts/hooks/use-story-markers.ts @@ -0,0 +1,24 @@ +import {useSelector} from 'react-redux'; + +import {selectedLayerIdsSelector} from '../selectors/layers/selected-ids'; +import {StoriesStateSelector} from '../selectors/story/story-state'; + +export const useStoryMarkers = () => { + const selectedLayers = useSelector(selectedLayerIdsSelector); + const stories = useSelector(StoriesStateSelector).list; + const hideMarkers = Boolean( + selectedLayers.mainId || selectedLayers.compareId + ); + + if (hideMarkers) { + return []; + } + + const storyMarkers = stories.map(story => ({ + id: story.id, + title: story.title, + position: story.position + })); + + return storyMarkers; +}; diff --git a/src/scripts/libs/create-marker.ts b/src/scripts/libs/create-marker.ts new file mode 100644 index 000000000..bf678d953 --- /dev/null +++ b/src/scripts/libs/create-marker.ts @@ -0,0 +1,218 @@ +import { + Cartesian3, + BillboardGraphics, + Entity, + ConstantProperty, + VerticalOrigin, + HorizontalOrigin, + Cartesian2 +} from 'cesium'; + +import NotesEsaBold from '../../../assets/fonts/NotesEsaBol.otf'; + +import {Marker} from '../types/marker-type'; + +export async function createMarker(marker: Marker): Promise { + const canvas = document.createElement('canvas'); + canvas.width = 700; + canvas.height = 300; + + const svgString = await getSvgString( + unescape(encodeURIComponent(marker.title)) + ); + + const image = new Image(); + image.src = `data:image/svg+xml;base64,${window.btoa(svgString)}`; + + return new Promise(resolve => { + image.onload = function() { + // @ts-ignore + canvas.getContext('2d').drawImage(image, 0, 0); + + resolve( + new Entity({ + id: `${marker.id}`, + position: Cartesian3.fromDegrees( + marker.position[0], + marker.position[1] + ), + billboard: new BillboardGraphics({ + image: new ConstantProperty(canvas), + verticalOrigin: new ConstantProperty(VerticalOrigin.TOP), + horizontalOrigin: new ConstantProperty(HorizontalOrigin.LEFT), + pixelOffset: new ConstantProperty(new Cartesian2(0, 0)) + }) + }) + ); + }; + }); +} + +let fontPromise: Promise | null = null; + +async function loadFont() { + if (fontPromise) { + return fontPromise; + } + const response = await fetch(NotesEsaBold); + const blob = await response.blob(); + const reader = new FileReader(); + + fontPromise = new Promise(resolve => { + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsDataURL(blob); + }); + + return fontPromise; +} + +async function getSvgString(storyTitle: string) { + const base64font = await loadFont(); + + return ` + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ ${storyTitle} +
+
+
+
+
+
`; +} diff --git a/src/scripts/types/marker-type.ts b/src/scripts/types/marker-type.ts new file mode 100644 index 000000000..c82c47a30 --- /dev/null +++ b/src/scripts/types/marker-type.ts @@ -0,0 +1,5 @@ +export interface Marker { + id: string; + title: string; + position: number[]; +} diff --git a/src/scripts/types/story-list.d.ts b/src/scripts/types/story-list.d.ts index 58c9d23cc..cd52416d3 100644 --- a/src/scripts/types/story-list.d.ts +++ b/src/scripts/types/story-list.d.ts @@ -5,6 +5,7 @@ export interface StoryListItem { link: string; image: string; tags: string[]; + position: number[]; } export type StoryList = StoryListItem[];