import { Fill, Text, Icon, Stroke, Style, Circle } from 'ol/style'; import startIcon from '../../icons/flag-start-b-svgrepo-com-green.svg'; import finishIcon from '../../icons/flag-finish-b-o-svgrepo-com-red.svg'; import wptIcon from '../../icons/location-pin-svgrepo-com-green.svg'; import houseIcon from '../../icons/house-svgrepo-com.svg'; import houseFlatIcon from '../../icons/houseFlat-svgrepo-com.svg'; import campingIcon from '../../icons/camping-14-svgrepo-com.svg'; import farmPigIcon from '../../icons/farm-pig-svgrepo-com.svg'; import cheeseIcon from '../../icons/cheese-svgrepo-com.svg'; import trainIcon from '../../icons/train-svgrepo-com.svg'; import picnicIcon from '../../icons/picnic-svgrepo-com.svg'; import caveIcon from '../../icons/cave-entrance-svgrepo-com.svg'; import leftArrowIcon from '../../icons/right-arrow-svgrepo-com.svg'; import blackArrowheadPointingUp from '../../icons/black-arrowhead-pointing-up-svgrepo-com.svg'; import wptIconSel from '../../icons/location-pin-svgrepo-com-red.svg'; import { Feature } from 'ol'; import memoize from 'memoizee'; import { getMap, getState } from '../map'; import { Point } from 'ol/geom'; import { Coordinate } from 'ol/coordinate'; import { createDefaultStyle } from 'ol/style/Style'; import osmIcons, { highlight } from './osm-icons'; import { indexOf } from 'lodash'; import { getZoomInteger } from '../map/Map'; import { isHighlighted } from '../map-tile-provider'; import { getAllPoiTypes, getHighlightedTagValue, getTagValue, } from '../map-tile-provider/MapTileProvider'; import { getCenter } from 'ol/extent'; import { getVectorTileFeatureType } from '../overlays/overlay-definitions'; interface StyleParameters { type: string; isSelected: boolean; } getAllPoiTypes().forEach((type) => { if (!Object.keys(osmIcons).includes(type)) { console.warn({ caller: 'styles', message: 'missing icon', type }); } }); const icons = { house: { src: houseIcon, scale: 1 / 15, opacity: 0.9, anchor: [0.5, 1], }, houseFlat: { src: houseFlatIcon, scale: 1 / 15, opacity: 0.9, anchor: [0.5, 1], }, camping: { src: campingIcon, scale: 2, opacity: 0.9, anchor: [0.5, 1], }, farmPig: { src: farmPigIcon, scale: 1 / 12, opacity: 0.9, anchor: [0.5, 1], }, cheese: { src: cheeseIcon, scale: 3 / 4, opacity: 0.9, anchor: [0.5, 1], }, train: { src: trainIcon, scale: 1 / 10, opacity: 0.9, anchor: [0.5, 1], }, picnic: { src: picnicIcon, scale: 1 / 15, opacity: 0.9, anchor: [0.5, 1], }, cave: { src: caveIcon, scale: 1 / 7, opacity: 0.9, anchor: [0.5, 1], }, }; const wptIconObj = { src: wptIcon, scale: 0.1, opacity: 0.9, anchor: [0.5, 1], }; const textStroke = new Stroke({ color: '#fff', width: 3, }); const textStrokeSel = new Stroke({ color: 'blue', width: 2, }); const textFill = new Fill({ color: '#000', }); const textFillSel = new Fill({ color: 'red', }); const trksegStroke = new Stroke({ color: [11, 16, 71, 0.8], width: 3, }); const trksegStrokeSel = new Stroke({ color: 'red', width: 3, }); const rteStroke = new Stroke({ color: [18, 71, 11, 0.8], width: 3, }); const rteStrokeSel = new Stroke({ color: 'red', width: 3, }); const circleFill = new Fill({ color: 'rgba(255,255,255,0.4)', }); const circleStroke = new Stroke({ color: '#3399CC', width: 1.25, }); const poiTextFill = new Fill({ color: 'white', }); const circle = new Circle({ fill: circleFill, stroke: circleStroke, radius: 5, }); const replacer = (key: string, value: any) => { if (key === 'feature' && typeof value === 'object') { return { id: value.get('id'), rev: value.getRevision() }; } return value; }; const normalizer = (params: any) => { const key = JSON.stringify(params, replacer); // console.log({ caller: 'getStyle / normalizer', key }); return key; }; const memoizeOptions = { length: 1, normalizer, max: 1024000, }; const styles = { wpt: { getParameters: (feature: Feature) => { const minZoom = feature.get('extensions')?.['dyo:minZoom']; return { isSelected: feature.get('isSelected') ?? false, text: feature.get('name'), customIcon: icons[feature.get('sym') as keyof typeof icons], hidden: minZoom && getZoominteger() < minZoom, }; }, getStyle: memoize((params: any) => { // console.log({ caller: 'getStyle', params }); const { isSelected, text, customIcon, hidden } = params; if (hidden) { return null; } const icon = customIcon ?? wptIconObj; return new Style({ image: new Icon(icon), text: new Text({ font: '16px Calibri,sans-serif', text: text, fill: isSelected ? textFillSel : textFill, stroke: isSelected ? textStrokeSel : textStroke, offsetY: -40, }), }); }, memoizeOptions), }, trkseg: { getParameters: (feature: Feature) => { return { isSelected: feature.get('isSelected') ?? false, feature: getZoomInteger() >= 7 ? feature : undefined, zoom: getZoomInteger() >= 7 ? Math.floor(getZoomInteger()) : undefined, }; }, getStyle: memoize((params: any) => { // console.log({ caller: 'getStyle', params }); const { isSelected, feature, zoom } = params; const styles = [ new Style({ stroke: isSelected ? trksegStrokeSel : trksegStroke }), ]; if (feature) { const map = getMap(); const geometry = feature.getGeometry(); const coordinates = geometry.getCoordinates(); let start = coordinates[0]; let startPixels = map?.getPixelFromCoordinate(start); coordinates.slice(1).forEach((end: Coordinate) => { const endPixels = map?.getPixelFromCoordinate(end); if ( startPixels !== undefined && endPixels !== undefined && Math.sqrt( (startPixels[0] - endPixels[0]) ** 2 + (startPixels[1] - endPixels[1]) ** 2 ) > 80 ) { const dx = end[0] - start[0]; const dy = end[1] - start[1]; const rotation = Math.atan2(dy, dx) - Math.PI / 2; styles.push( new Style({ geometry: new Point(end), image: new Icon({ src: blackArrowheadPointingUp, scale: 1 / 20, anchor: [0.5, 0.5], rotateWithView: true, rotation: -rotation, }), }) ); startPixels = endPixels; start = end; } }); } return styles; }, memoizeOptions), }, rte: { getParameters: (feature: Feature) => { return { isSelected: feature.get('isSelected') ?? false, }; }, getStyle: memoize((params: any) => { // console.log({ caller: 'getStyle', params }); const { isSelected } = params; return new Style({ stroke: isSelected ? rteStrokeSel : rteStroke }); }, memoizeOptions), }, route: { getParameters: (feature: Feature) => { return { isSelected: feature.get('isSelected') ?? false, name: feature.get('name'), }; }, getStyle: memoize((params: any) => { console.log({ caller: 'getStyle', params }); const { isSelected, name } = params; return new Style({ stroke: isSelected ? routeStrokeSel : routeStroke, text: new Text({ text: name, font: 'bold 14px "Open Sans", "Arial Unicode MS", "sans-serif"', placement: 'line', padding: [2, 2, 2, 2], fill: new Fill({ color: 'black', }), }), }); }, memoizeOptions), }, way: { strokes: { iwn: new Stroke({ color: [174, 33, 219, 0.8], width: 6, }), nwn: new Stroke({ color: [174, 33, 219, 0.8], width: 5, }), rwn: new Stroke({ color: [174, 33, 219, 0.8], width: 5, lineDash: [10, 10], }), lwn: new Stroke({ color: [174, 33, 219, 0.8], width: 3, lineDash: [10, 10], }), default: new Stroke({ color: [174, 33, 219, 0.8], width: 1, lineDash: [10, 10], }), }, getParameters: (feature: Feature) => { const tags = JSON.parse(feature.get('tags')); if (getZoomInteger() < 12 && !['iwn', 'nwn'].includes(tags.network)) { return null; } if ( getZoomInteger() < 14 && !['iwn', 'nwn', 'lwn'].includes(tags.network) ) { return null; } return { isSelected: feature.get('isSelected') ?? false, name: feature.get('name'), network: tags.network, }; }, getStyle: memoize((params: any) => { // console.log({ caller: 'getStyle', params }); const { isSelected, name, network } = params; return new Style({ stroke: styles.way.strokes[network] || styles.way.strokes.default, text: new Text({ text: name, font: 'bold 14px "Open Sans", "Arial Unicode MS", "sans-serif"', placement: 'line', padding: [2, 2, 2, 2], fill: new Fill({ color: 'black', }), }), }); }, memoizeOptions), }, poi: { getParameters: (feature: Feature) => { if (feature.getGeometryName() !== 'Point') { feature.setGeometry( new Point(getCenter(feature.getGeometry().getExtent())) ); } if (getZoomInteger() < 14) { return null; } const highlightedTagValue = getHighlightedTagValue(feature); const isHighlightedFeature = !!highlightedTagValue; if ( (isHighlightedFeature && getZoomInteger() < 14) || (!isHighlightedFeature && getZoomInteger() < 16) ) { return null; } return { name: feature.get('name'), poiType: isHighlightedFeature ? highlightedTagValue : getTagValue(feature), isHighlighted: isHighlightedFeature, isTextHidden: getZoomInteger() < 19, }; }, getStyle: memoize((params: any) => { // console.log({ caller: 'getStyle', params }); const { name, poiType, isTextHidden, isHighlighted } = params; const icon = osmIcons[poiType]; if (icon === undefined) { return undefined; } return [ new Style({ image: new Circle({ radius: 28, fill: new Fill({ color: isHighlighted ? [174, 33, 219, 0.7] : [255, 255, 255, 0.3], }), displacement: isHighlighted ? [0, +0] : [0, +3], }), }), new Style({ image: new Icon({ src: icon, scale: isHighlighted ? 2 : 1, opacity: 1, color: isHighlighted ? 'red' : 'black', // anchor: [0, 0], }), text: name && !isTextHidden ? new Text({ text: name, font: 'bold 14px "Open Sans", "Arial Unicode MS", "sans-serif"', offsetY: +40, padding: [0, 0, 0, 0], fill: new Fill({ color: 'black', }), backgroundFill: poiTextFill, }) : undefined, }), ]; }, memoizeOptions), }, cluster: { getParameters: (feature: Feature) => { const nbFeatures = feature.get('features').length; // console.log({ caller: 'cluster / getParameters', feature, nbFeatures }); return { nbFeatures, }; }, getStyle: memoize((params: any) => { // console.log({ caller: 'cluster / getStyle', params }); const { nbFeatures } = params; return new Style({ image: new Circle({ radius: 12, fill: new Fill({ color: [174, 33, 219, 0.7] }), }), text: new Text({ text: `${nbFeatures}`, font: 'bold 18px "Open Sans", "Arial Unicode MS", "sans-serif"', offsetY: +2, padding: [0, 0, 0, 0], fill: new Fill({ color: 'black', }), }), }); }, memoizeOptions), }, }; export const style = (feature: Feature, resolution: number) => { // if (feature.get('osm_id') === '990113898') { // console.log({ caller: 'style', message: 'Found feature 990113898' }); // } const type = ( feature.get('type') !== undefined ? feature.get('type') : getVectorTileFeatureType(feature) ) as keyof typeof styles; const styleForType = styles[type]; if (!styleForType) { // console.log({ caller: 'style / default', type, feature, resolution }); // return createDefaultStyle(feature, resolution)[0]; return null; } const params = styles[type].getParameters(feature); if (params === null || params?.isHidden) { return null; } // if (params.subFeature) { // return style(params.subFeature, resolution); // } const getStyle = styles[type].getStyle; const result = getStyle(params); // console.log({ caller: 'style', feature, type, params, style }); if (result === undefined) { // console.log({ caller: 'style / unknown', feature }); return createDefaultStyle(feature, resolution)[0]; } return result; }; export default style;