dyomedea/src/components/gpx/styles.ts

695 lines
18 KiB
TypeScript

import { Fill, Text, Icon, Stroke, Style, Circle } from 'ol/style';
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 hikerIcon from '../../icons/hiker-svgrepo-com.svg';
import blackArrowheadPointingUp from '../../icons/black-arrowhead-pointing-up-svgrepo-com.svg';
import noteIcon from '../../icons/note-svgrepo-com.svg';
import { Feature } from 'ol';
import memoize from 'memoizee';
import { getMap } from '../map';
import { Point } from 'ol/geom';
import { Coordinate } from 'ol/coordinate';
import { createDefaultStyle } from 'ol/style/Style';
import osmIcons from './osm-icons';
import { getZoomInteger } from '../map/Map';
import { isHighlightedTagType } from '../map-tile-provider/MapTileProvider';
import { getCenter } from 'ol/extent';
import {
getVectorTileFeatureType,
getTagType,
} from '../overlays/overlay-definitions';
import { Category } from '../wpt/WptEditDialog';
interface StyleParameters {
type: string;
isSelected: boolean;
}
const icons = {
note: {
src: noteIcon,
scale: 1 / 20,
opacity: 0.9,
anchor: [0.5, 1],
},
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 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 extensions = feature.get('extensions');
const minZoom = extensions?.['dyo:minZoom'];
const category = extensions?.category;
return {
isSelected: feature.get('isSelected') ?? false,
text: feature.get('name'),
customIcon:
icons[feature.get('sym') as keyof typeof icons] ||
icons[category as keyof typeof icons],
thumbnailUrl:
category === Category.PICTURE ? extensions?.thumbnailUrl : undefined,
hidden: minZoom && getZoominteger() < minZoom,
};
},
getStyle: memoize((params: any) => {
// console.log({ caller: 'getStyle', params });
const { isSelected, text, customIcon, hidden, thumbnailUrl } = params;
if (hidden) {
return null;
}
const thumbnailIcon = !!thumbnailUrl
? { src: thumbnailUrl, scale: 1 / 2, anchor: [0.5, 0.5] }
: undefined;
const icon = customIcon ?? thumbnailIcon ?? 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,
};
},
ownStyles: {
normal: [
new Style({
stroke: new Stroke({
color: [44, 64, 255, 0.8],
width: 2,
}),
}),
new Style({
stroke: new Stroke({
color: [44, 64, 255, 0.2],
width: 7,
}),
}),
],
selected: [
new Style({
stroke: new Stroke({
color: [44, 64, 255, 0.8],
width: 2,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.4],
width: 7,
}),
}),
],
},
getStyle: memoize((params: any) => {
// console.log({ caller: 'getStyle', params });
const { isSelected, feature, zoom } = params;
let currentStyles: any = isSelected
? styles.trkseg.ownStyles.selected
: styles.trkseg.ownStyles.normal;
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;
currentStyles.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 currentStyles;
}, memoizeOptions),
},
'trkseg-finish': {
getParameters: (feature: Feature) => {
const positions: Set<string> = feature.get('positions');
return {
isLast: positions.has('gpx-end') && positions.has('trk-end'),
};
},
getStyle: memoize((params: any) => {
// console.log({ caller: 'getStyle', params });
const { isLast } = params;
if (isLast) {
return new Style({
image: new Icon({
src: hikerIcon,
scale: 1 / 20,
opacity: 0.9,
anchor: [0.5, 0.5],
}),
});
}
return null;
}, 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),
},
way: {
strokes: {
veryLowZoom: {
iwn: [
new Style({
stroke: new Stroke({
color: [255, 0, 0, 1],
width: 1,
}),
}),
],
nwn: [
new Style({
stroke: new Stroke({
color: [255, 0, 0, 1],
width: 1,
}),
}),
],
rwn: [
new Style({
stroke: new Stroke({
color: [255, 128, 0, 1],
width: 1,
}),
}),
],
lwn: [
new Style({
stroke: new Stroke({
color: [255, 255, 0, 1],
width: 1,
}),
}),
],
default: [
new Style({
stroke: new Stroke({
color: [255, 255, 0, 1],
width: 1,
lineDash: [10, 10],
}),
}),
],
},
lowZoom: {
iwn: [
new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.6],
width: 3,
}),
}),
],
nwn: [
new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.6],
width: 3,
}),
}),
],
rwn: [
new Style({
stroke: new Stroke({
color: [255, 128, 0, 0.6],
width: 3,
}),
}),
],
lwn: [
new Style({
stroke: new Stroke({
color: [255, 255, 0, 0.6],
width: 3,
lineDash: [10, 10],
}),
}),
],
default: [
new Style({
stroke: new Stroke({
color: [255, 255, 0, 0.6],
width: 1,
lineDash: [10, 10],
}),
}),
],
},
highZoom: {
iwn: [
new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.6],
width: 7,
// lineDash: [10, 10],
// lineDashOffset: 10,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 255, 255, 0.6],
width: 3,
// lineDash: [10, 10],
// lineDashOffset: 0,
}),
}),
],
nwn: [
new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.6],
width: 7,
// lineDash: [10, 10],
// lineDashOffset: 10,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 255, 255, 0.6],
width: 3,
// lineDash: [10, 10],
// lineDashOffset: 0,
}),
}),
],
rwn: [
new Style({
stroke: new Stroke({
color: [255, 255, 0, 0.6],
width: 7,
// lineDash: [10, 10],
// lineDashOffset: 10,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 0, 0, 0.6],
width: 3,
// lineDash: [10, 10],
// lineDashOffset: 0,
}),
}),
],
lwn: [
new Style({
stroke: new Stroke({
color: [0, 0, 255, 0.6],
width: 3,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 255, 0, 0.6],
width: 1,
}),
}),
],
default: [
new Style({
stroke: new Stroke({
color: [0, 0, 255, 0.6],
lineDash: [10, 10],
width: 3,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 255, 0, 0.6],
lineDash: [10, 10],
width: 1,
}),
}),
],
},
},
getParameters: (feature: Feature) => {
// let tags = feature.get('tags');
// try {
// tags = JSON.parse(feature.get('tags'));
// } catch (error) {
// tags = { error };
// console.error({
// caller: 'style / JSON.parse',
// tags: feature.get('tags'),
// error,
// feature,
// });
// }
const network = feature.get('network');
if (getZoomInteger() < 7 && !['iwn'].includes(network)) {
return null;
}
if (getZoomInteger() < 11 && !['iwn', 'nwn'].includes(network)) {
return null;
}
if (getZoomInteger() < 13 && !['iwn', 'nwn', 'lwn'].includes(network)) {
return null;
}
const name =
getZoomInteger() < 15
? feature.get('ref')
: feature.get('name') || feature.get('ref');
return {
// isSelected: feature.get('isSelected') ?? false,
name: !!name ? name : undefined,
network,
zoom:
getZoomInteger() >= 12
? 'highZoom'
: getZoomInteger() >= 8
? 'lowZoom'
: 'veryLowZoom',
};
},
getStyle: memoize((params: any) => {
// console.log({ caller: 'getStyle', params });
const { name, network, zoom } = params;
return [
...(styles.way.strokes[zoom][network] ??
styles.way.strokes[zoom].default),
new Style({
text: new Text({
text: name,
font: 'bold 14px "Open Sans", "Arial Unicode MS", "sans-serif"',
placement: 'line',
overflow: false,
maxAngle: Math.PI / 16,
textBaseline: ['iwn', 'nwn'].includes(network) ? 'top' : 'bottom',
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() < 16) {
return null;
}
const tagType = getTagType(feature);
const isHighlightedFeature = isHighlightedTagType(tagType);
if (
(isHighlightedFeature && getZoomInteger() < 16) ||
(!isHighlightedFeature && getZoomInteger() < 18)
) {
return null;
}
return {
name: feature.get('name'),
poiType: tagType,
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) => {
// console.log({ caller: 'style', feature, values: feature.values_ });
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);
// console.log({
// caller: 'style',
// type,
// styleForType,
// params,
// feature,
// resolution,
// });
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;