Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geoscribble #1373

Merged
merged 2 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ en:
osmose:
tooltip: Data issues detected by osmose.openstreetmap.fr
title: Osmose Issues
geoScribble:
tooltip: Freehand annotations done by EveryDoor users
title: GeoScribble Annotations
custom:
tooltip: "Drag and drop a data file onto the page, or click the button to setup"
title: Custom Map Data
Expand Down
62 changes: 4 additions & 58 deletions modules/pixi/PixiLayerCustomData.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as PIXI from 'pixi.js';
import { gpx, kml } from '@tmcw/togeojson';
import { Extent } from '@rapid-sdk/math';
import { geojsonExtent, geojsonFeatures } from '../util/util.js';
import geojsonRewind from '@mapbox/geojson-rewind';

import { AbstractLayer } from './AbstractLayer.js';
Expand Down Expand Up @@ -88,7 +88,7 @@ export class PixiLayerCustomData extends AbstractLayer {
}
geoData = vtService.getData(this._template).map(d => d.geojson);
} else {
geoData = this._getFeatures(this._geojson);
geoData = geojsonFeatures(this._geojson);
}

const polygons = geoData.filter(d => d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon');
Expand Down Expand Up @@ -510,7 +510,7 @@ export class PixiLayerCustomData extends AbstractLayer {
this._dataUsed = `${extension} data file`;
this._geojson = this._ensureIDs(geojson);
geojsonRewind(this._geojson);
this._geojsonExtent = this._calcExtent(geojson);
this._geojsonExtent = geojsonExtent(geojson);
this.fitZoom();
this.scene.enableLayers(this.layerID); // emits 'layerchange', so UI gets updated
}
Expand All @@ -531,7 +531,7 @@ export class PixiLayerCustomData extends AbstractLayer {
_ensureIDs(geojson) {
if (!geojson) return null;

for (const feature of this._getFeatures(geojson)) {
for (const feature of geojsonFeatures(geojson)) {
this._ensureFeatureID(feature);
}
return geojson;
Expand All @@ -556,60 +556,6 @@ export class PixiLayerCustomData extends AbstractLayer {
}


/**
* _getFeatures
* The given GeoJSON may be a single Feature or a FeatureCollection.
* Here we expand it to an Array of Features.
* @return {Array} GeoJSON Features
*/
_getFeatures(geojson) {
if (!geojson) return [];
return (geojson.type === 'FeatureCollection') ? geojson.features : [geojson];
}


/**
* _calcExtent
* @param {Object} geojson - a GeoJSON Feature or FeatureCollection
* @return {Extent}
*/
_calcExtent(geojson) {
const extent = new Extent();
if (!geojson) return extent;

for (const feature of this._getFeatures(geojson)) {
const geometry = feature.geometry;
if (!geometry) continue;

const type = geometry.type;
const coords = geometry.coordinates;

// Treat single types as multi types to keep the code simple
const parts = /^Multi/.test(type) ? coords : [coords];

if (/Polygon$/.test(type)) {
for (const polygon of parts) {
const outer = polygon[0]; // No need to iterate over inners
for (const point of outer) {
extent.extendSelf(point);
}
}
} else if (/LineString$/.test(type)) {
for (const line of parts) {
for (const point of line) {
extent.extendSelf(point);
}
}
} else if (/Point$/.test(type)) {
for (const point of parts) {
extent.extendSelf(point);
}
}
}

return extent;
}


/**
* _getExtension
Expand Down
238 changes: 238 additions & 0 deletions modules/pixi/PixiLayerGeoScribble.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import * as PIXI from 'pixi.js';
import { Color } from 'pixi.js';

import { AbstractLayer } from './AbstractLayer.js';
import { PixiFeatureLine } from './PixiFeatureLine.js';
import { PixiFeaturePoint } from './PixiFeaturePoint.js';

const CUSTOM_COLOR = 0x2eff2e;


/**
* PixiLayerGeoScribble
* This class contains any geo scribbles that should be 'drawn over' the map.
* Originally from the EveryDoor folks - reference: https://github.com/Zverik/every_door/issues/197
* This data comes from API at https://geoscribble.osmz.ru/docs#/default/scribbles_scribbles_get.
* @class
*/
export class PixiLayerGeoScribble extends AbstractLayer {

/**
* @constructor
* @param scene The Scene that owns this Layer
* @param layerID Unique string to use for the name of this Layer
*/
constructor(scene, layerID) {
super(scene, layerID);


const geoscribbles = new PIXI.Container();
geoscribbles.name = `${this.layerID}-geoscribbles`;
geoscribbles.sortableChildren = false;
geoscribbles.interactiveChildren = false;
this.scribblesContainer = geoscribbles;

const basemapContainer = this.scene.groups.get('basemap');
basemapContainer.addChild(geoscribbles);


}

/**
* supported
* Whether the Layer's service exists
*/
get supported() {
return !!this.context.services.geoScribble;
}


/**
* enabled
* Whether the user has chosen to see the Layer
* Make sure to start the service.
*/
get enabled() {
return this._enabled;
}
set enabled(val) {
if (!this.supported) {
val = false;
}

if (val === this._enabled) return; // no change
this._enabled = val;

if (val) {
this.dirtyLayer();
this.context.services.geoScribble.startAsync();
}
}


/**
* render
* Render the geojson custom data
* @param frame Integer frame being rendered
* @param viewport Pixi viewport to use for rendering
* @param zoom Effective zoom to use for rendering
*/
render(frame, viewport, zoom) {
if (!this.enabled) return;

const gsService = this.context.services.geoScribble;
gsService.loadTiles();

const geoData = gsService.getData();

// No polygons will be returned by the service, so we don't need to consider those types.
const lines = geoData.filter(d => d.geometry.type === 'LineString' || d.geometry.type === 'MultiLineString');
const points = geoData.filter(d => d.geometry.type === 'Point' || d.geometry.type === 'MultiPoint');

this.renderLines(frame, viewport, zoom, lines);
this.renderPoints(frame, viewport, zoom, points);
}

/**
* getLineStyle
* @param styleOverride Custom style
* @param line the geojson formatted scribble object with the following useful (but optional) style properties:
* `thin` (boolean)
* `dashed` (boolean)
* `color` (hex code string like `#FFEECC`)
* `style` One of: "scribble", "eraser", "road", "track", "footway", "path", "cycleway", "cycleway_shared",
* "wall", "fence", "power","stream", "drain", etc.
* @return a style object that can be given to the pixi renderer
*/

getLineStyle(styleOverride, line) {
// Start with the default style object.
const lineStyle = styleOverride || {
stroke: { width: 2, color: CUSTOM_COLOR, alpha: 1, cap: PIXI.LINE_CAP.ROUND },
labelTint: CUSTOM_COLOR
};

const color = line.properties.color ? new Color(line.properties.color) : CUSTOM_COLOR;
const thin = line.properties.thin;
const dashed = line.properties.dashed;

//Modify the alpha down a bit to add to 'scribble' factor.
lineStyle.stroke.alpha = 0.70;
lineStyle.stroke.color = color;
lineStyle.stroke.width = thin ? 4 : 8;
if (dashed) {
lineStyle.stroke.dash = thin ? [12,6] : [24,12]; // Thinner lines get shorter dashes
}
return lineStyle;
}

/**
* renderLines
* @param frame Integer frame being rendered
* @param viewport Pixi viewport to use for rendering
* @param zoom Effective zoom to use for rendering
* @param lines Array of line data
* @param styleOverride Custom style
*/
renderLines(frame, viewport, zoom, lines, styleOverride) {
const l10n = this.context.systems.l10n;
const parentContainer = this.scribblesContainer;


for (const d of lines) {
const dataID = d.__featurehash__;
const version = d.v || 0;
const parts = (d.geometry.type === 'LineString') ? [d.geometry.coordinates]
: (d.geometry.type === 'MultiLineString') ? d.geometry.coordinates : [];

for (let i = 0; i < parts.length; ++i) {
const coords = parts[i];
const featureID = `${this.layerID}-${dataID}-${i}`;
let feature = this.features.get(featureID);

// If feature existed before as a different type, recreate it.
if (feature && feature.type !== 'line') {
feature.destroy();
feature = null;
}

const lineStyle = this.getLineStyle(styleOverride, d);
if (!feature) {
feature = new PixiFeatureLine(this, featureID);
feature.style = lineStyle;
feature.parentContainer = parentContainer;
}

// If data has changed.. Replace it.
if (feature.v !== version) {
feature.v = version;
feature.geometry.setCoords(coords);
feature.label = l10n.displayName(d.properties);
feature.setData(dataID, d);
}

this.syncFeatureClasses(feature);
feature.update(viewport, zoom);
this.retainFeature(feature, frame);
}
}
}


/**
* renderPoints
* @param frame Integer frame being rendered
* @param viewport Pixi viewport to use for rendering
* @param zoom Effective zoom to use for rendering
* @param lines Array of point data
*/
renderPoints(frame, viewport, zoom, points) {
const l10n = this.context.systems.l10n;
const parentContainer = this.scribblesContainer;

const pointStyle = {
markerName: 'largeCircle',
markerTint: CUSTOM_COLOR,
iconName: 'maki-circle-stroked',
labelTint: CUSTOM_COLOR
};

for (const d of points) {
const dataID = d.__featurehash__;
const version = d.v || 0;
const parts = (d.geometry.type === 'Point') ? [d.geometry.coordinates]
: (d.geometry.type === 'MultiPoint') ? d.geometry.coordinates : [];

for (let i = 0; i < parts.length; ++i) {
const coords = parts[i];
const featureID = `${this.layerID}-${dataID}-${i}`;
let feature = this.features.get(featureID);

// If feature existed before as a different type, recreate it.
if (feature && feature.type !== 'point') {
feature.destroy();
feature = null;
}

if (!feature) {
feature = new PixiFeaturePoint(this, featureID);
feature.style = pointStyle;
feature.parentContainer = parentContainer;
}

// If data has changed.. Replace it.
if (feature.v !== version) {
feature.v = version;
feature.geometry.setCoords(coords);
feature.label = l10n.displayName(d.properties);
feature.setData(dataID, d);
}

this.syncFeatureClasses(feature);
feature.update(viewport, zoom);
this.retainFeature(feature, frame);
}
}
}

}
2 changes: 2 additions & 0 deletions modules/pixi/PixiScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PixiLayerOsmNotes } from './PixiLayerOsmNotes.js';
import { PixiLayerOsmose } from './PixiLayerOsmose.js';
import { PixiLayerRapid } from './PixiLayerRapid.js';
import { PixiLayerStreetsidePhotos } from './PixiLayerStreetsidePhotos.js';
import { PixiLayerGeoScribble } from './PixiLayerGeoScribble.js';


// Convert a single value, an Array of values, or a Set of values.
Expand Down Expand Up @@ -88,6 +89,7 @@ export class PixiScene extends EventEmitter {
// Create Layers
[
new PixiLayerBackgroundTiles(this, 'background'),
new PixiLayerGeoScribble(this, 'geoScribble'),
new PixiLayerOsm(this, 'osm'),
new PixiLayerRapid(this, 'rapid'),

Expand Down
Loading