massive Shape rework: keep both tvPoints/Props and ourPoints/Props; delegate model updates to subclasses; DCA/VLine working but Ladder/HLine not done.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import {invokeCallback, mixin} from "@/common.js";
|
||||
import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape, widget} from "@/charts/chart.js";
|
||||
import {unique} from "@/misc.js";
|
||||
import {useChartOrderStore} from "@/orderbuild.js";
|
||||
import model from "color";
|
||||
|
||||
|
||||
//
|
||||
@@ -37,34 +39,96 @@ export const ShapeType = {
|
||||
}
|
||||
|
||||
|
||||
class Shape {
|
||||
constructor(type, model={}, onModel=null, onDelete=null, props=null) {
|
||||
export class Shape {
|
||||
|
||||
constructor(type, onModel=null, onDelete=null, props=null) {
|
||||
// the Shape object manages synchronizing internal data with a corresponding TradingView shape
|
||||
|
||||
// each shape in the class hierarchy overrides setModel() to cause effects in TradingView
|
||||
// TV callbacks are returned to the on*() handlers, primarily onPoints() and onProps(). Handlers
|
||||
// of TV callbacks should call updateModel() with just the kv's they want to change. Any changes
|
||||
// are then passed onto the onModel() callback.
|
||||
|
||||
// the model object has various attributes defined by each shape subclass. each subclass's constructor
|
||||
// must call setModel(model) after first calling super.constructor() and optionally declaring any model
|
||||
// defaults on this.model.
|
||||
|
||||
// Shape: {
|
||||
// lineColor, textColor, color, // lineColor and textColor default to color
|
||||
// }
|
||||
|
||||
this.debug = false
|
||||
this.id = null // TradingView shapeId, or null if no TV shape created yet (drawing mode)
|
||||
this.type = type // ShapeType
|
||||
this.model = model // subclass-specific
|
||||
this.points = null
|
||||
this.points = this.pointsFromModel()
|
||||
// console.log('construct points', this.points)
|
||||
this.props = props === null ? this.propsFromModel() : mixin(props, this.propsFromModel())
|
||||
this.model = {} // set to nothing at first
|
||||
this.ourPoints = null
|
||||
this.tvPoints = null
|
||||
this.ourProps = {}
|
||||
if (props !== null)
|
||||
this.ourProps = mixin(props, this.ourProps)
|
||||
this.tvProps = null
|
||||
if (onModel !== null)
|
||||
this.onModel = onModel
|
||||
if (onDelete !== null )
|
||||
this.onDelete = onDelete
|
||||
this.create()
|
||||
|
||||
// Model values handled by this base class
|
||||
this.model.color = null
|
||||
this.model.lineColor = null
|
||||
this.model.textColor = null
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// primary interface methods
|
||||
//
|
||||
|
||||
setModel(model) {
|
||||
if (model.textColor || model.lineColor) {
|
||||
if (model.textColor)
|
||||
this.model.textColor = model.textColor
|
||||
if (model.lineColor)
|
||||
this.model.lineColor = model.lineColor
|
||||
this.setProps(this.colorProps())
|
||||
}
|
||||
// todo text
|
||||
}
|
||||
|
||||
onModel(model, changedKeys) {} // model was changed by a TradingView user action
|
||||
|
||||
updateModel(changes) {
|
||||
if (this.debug) console.log('updateModel', this.id, changes)
|
||||
const changedKeys = []
|
||||
for (const k of Object.keys(changes)) {
|
||||
if( changes[k] !== undefined && changes[k] !== this.model[k] ) {
|
||||
changedKeys.push(k)
|
||||
this.model[k] = changes[k]
|
||||
}
|
||||
}
|
||||
if (changedKeys.length)
|
||||
this.onModel(this.model, changedKeys)
|
||||
}
|
||||
|
||||
colorProps() {
|
||||
if (!this.model.color&&!this.model.lineColor&&!this.model.textColor) return null
|
||||
const o = {}
|
||||
const p = this.type.drawingProp
|
||||
const lc = this.model.lineColor ? this.model.lineColor : this.model.color;
|
||||
const tc = this.model.textColor ? this.model.textColor : this.model.color;
|
||||
if (lc)
|
||||
o[p+".linecolor"] = lc
|
||||
if (tc)
|
||||
o[p+".textcolor"] = tc
|
||||
return o
|
||||
}
|
||||
|
||||
draw() {
|
||||
// have the user draw this shape
|
||||
console.log(`draw ${this.type.name}`, this.model)
|
||||
if (this.debug) console.log(`draw ${this.type.name}`, this.model)
|
||||
if (this.id)
|
||||
throw Error(`Shape already exists ${this.id}`)
|
||||
const or = this.drawingOverrides();
|
||||
// console.log('drawing overrides', or)
|
||||
// if (this.debug) console.log('drawing overrides', or)
|
||||
widget.applyOverrides(or)
|
||||
drawShape(this.type, new ShapeTVCallbacks(this))
|
||||
}
|
||||
@@ -72,30 +136,28 @@ class Shape {
|
||||
// return an object with property defaults, e.g. { "linetoolhorzline.linecolor": "#7f11e0" }
|
||||
// https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library#drawingoverrides
|
||||
drawingOverrides() {
|
||||
if (this.model.color===null) return null
|
||||
const o = {}
|
||||
const p = this.type.drawingProp
|
||||
o[p+".linecolor"] = this.model.color
|
||||
o[p+".textcolor"] = this.model.color
|
||||
return o
|
||||
return this.colorProps()
|
||||
}
|
||||
|
||||
create() {
|
||||
if (this.id !== null) return
|
||||
// programatically create the shape using the current this.points
|
||||
if( this.points && this.points.length ) {
|
||||
if( this.ourPoints && this.ourPoints.length ) {
|
||||
this.doCreate()
|
||||
}
|
||||
}
|
||||
|
||||
doCreate() {
|
||||
createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
|
||||
// createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
|
||||
this.tvPoints = [...this.ourPoints]
|
||||
this.id = createShape(this.type, this.ourPoints, {overrides:this.ourProps}, new ShapeTVCallbacks(this))
|
||||
if (this.debug) console.log('created', this.type.name, this.ourPoints, this.id)
|
||||
}
|
||||
|
||||
createOrDraw() {
|
||||
if(this.id) return
|
||||
if(this.points && this.points.length)
|
||||
this.doCreate(this.points)
|
||||
if(this.ourPoints && this.ourPoints.length)
|
||||
this.doCreate(this.ourPoints)
|
||||
else
|
||||
this.draw()
|
||||
}
|
||||
@@ -106,36 +168,25 @@ class Shape {
|
||||
}
|
||||
|
||||
|
||||
setModel(model, props=null) {
|
||||
for( const [k,v] of Object.entries(model))
|
||||
this.model[k] = v
|
||||
this.setPointsIfDirty(this.pointsFromModel());
|
||||
let p
|
||||
const mp = this.propsFromModel();
|
||||
if (props===null) {
|
||||
p = mp
|
||||
if (p===null)
|
||||
return
|
||||
}
|
||||
else if (mp !== null)
|
||||
p = mixin(props, mp)
|
||||
this.setPropsIfDirty(p)
|
||||
}
|
||||
|
||||
|
||||
setPoints(points) {
|
||||
this.points = points
|
||||
// setting points to null will delete the shape from the chart. setting points to a valid value will cause the
|
||||
// shape to be drawn.
|
||||
if (this.debug) console.log('setPoints', points)
|
||||
this.ourPoints = points
|
||||
if (points === null || !points.length)
|
||||
this.delete()
|
||||
else {
|
||||
if (this.id === null)
|
||||
this.doCreate()
|
||||
else {
|
||||
else if (dirtyPoints(this.tvPoints, points)) {
|
||||
const s = this.tvShape();
|
||||
if (!dragging) {
|
||||
if (this.debug) console.log('adjusting tv points', s, this.tvPoints, points)
|
||||
if (!this.beingDragged()) {
|
||||
if (this.debug) console.log('not dragging. use setPoints.')
|
||||
s.setPoints(points)
|
||||
}
|
||||
else {
|
||||
if (this.debug) console.log('dragging. use QUIET setPoints.')
|
||||
// quiet setPoints doesn't disturb tool editing mode
|
||||
const i = s._pointsConverter.apiPointsToDataSource(points)
|
||||
// s._model.startChangingLinetool(this._source)
|
||||
@@ -147,67 +198,39 @@ class Shape {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setPointsIfDirty(points) {
|
||||
if (dirtyPoints(this.points, points))
|
||||
this.setPoints(points)
|
||||
}
|
||||
|
||||
onPoints(points) {} // the control points of an existing shape were changed
|
||||
|
||||
setProps(props) {
|
||||
if(this.id)
|
||||
this.tvShape().setProperties(props)
|
||||
if (!props || Object.keys(props).length===0) return
|
||||
if (this.debug) console.log('setProps', props)
|
||||
this.ourProps = mixin(props, this.ourProps)
|
||||
if(this.id) {
|
||||
const p = dirtyItems(this.tvProps, props)
|
||||
this.tvProps = this.tvProps === null ? p : mixin(p, this.tvProps)
|
||||
this.tvShape().setProperties(p)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setPropsIfDirty(props) {
|
||||
// console.log('dirtyProps', this.props, props, dirtyProps(this.props, props))
|
||||
if( dirtyProps(this.props, props) )
|
||||
this.setProps(props)
|
||||
onProps(props) { // the display properties of an existing shape were changed
|
||||
this.updateModel({lineColor:props.linecolor, textColor:props.textcolor})
|
||||
}
|
||||
|
||||
|
||||
beingDragged() {
|
||||
return draggingShapeIds.indexOf(this.id) !== -1
|
||||
}
|
||||
|
||||
|
||||
delete() {
|
||||
// console.log('shape.delete', this.id)
|
||||
if (this.debug) console.log('shape.delete', this.id)
|
||||
this.ourPoints = null
|
||||
if (this.id === null) return
|
||||
this.lock++
|
||||
try {
|
||||
deleteShapeId(this.id)
|
||||
this.id = null
|
||||
} finally {
|
||||
this.lock--
|
||||
}
|
||||
deleteShapeId(this.id)
|
||||
this.id = null
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Model synchronization
|
||||
//
|
||||
|
||||
onDirtyModel(toModel) {
|
||||
const old = {...this.model}
|
||||
toModel.call(this)
|
||||
const dirty = dirtyKeys(old, this.model)
|
||||
// console.log('onDirtyModel', old, this.model, dirty)
|
||||
if (dirty.length)
|
||||
this.onModel(this.model)
|
||||
}
|
||||
|
||||
pointsFromModel() {return null}
|
||||
pointsToModel() {} // set the model using this.points
|
||||
propsFromModel() {return null}
|
||||
propsToModel() {} // set the model using this.props
|
||||
|
||||
onModel(model) {} // called whenever points or props updates the model dictionary
|
||||
|
||||
|
||||
//
|
||||
// Overridable shape callbacks
|
||||
// Overridable shape callbacks initiated by TradingView
|
||||
//
|
||||
|
||||
onDraw() {} // start drawing a new shape for the builder
|
||||
@@ -215,8 +238,6 @@ class Shape {
|
||||
onUndraw() {} // drawing was canceled by clicking on a different tool
|
||||
onAddPoint() {} // the user clicked a point while drawing (that point is added to the points list)
|
||||
onCreate(points, props) {} // the user has finished creating all the control points. drawing mode is exited and the initial shape is created.
|
||||
onPoints(points) {} // the control points of an existing shape were changed
|
||||
onProps(props) {} // the display properties of an existing shape were changed
|
||||
onMove(points) {} // the shape was moved by dragging a drawing element not the control point
|
||||
onDrag(points) {}
|
||||
onHide(props) {}
|
||||
@@ -226,7 +247,6 @@ class Shape {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function dirtyPoints(pointsA, pointsB) {
|
||||
if (pointsB===null)
|
||||
return pointsA !== null
|
||||
@@ -243,21 +263,6 @@ function dirtyPoints(pointsA, pointsB) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// B is modifying A
|
||||
function dirtyProps(propsA, propsB) {
|
||||
if (propsB===null)
|
||||
return propsA !== null
|
||||
const entries = Object.entries(propsB);
|
||||
if (propsA===null)
|
||||
return entries.length > 0
|
||||
for( const [k,v] of entries)
|
||||
if ( !(k in propsA) || propsA[k] !== v )
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// B is modifying A
|
||||
function dirtyKeys(propsA, propsB) {
|
||||
if (propsB===null)
|
||||
@@ -265,7 +270,15 @@ function dirtyKeys(propsA, propsB) {
|
||||
if (propsA===null)
|
||||
return [...Object.keys(propsB)]
|
||||
const keys = unique([...Object.keys(propsA), ...Object.keys(propsB)])
|
||||
return keys.filter((k)=> !(k in propsA) || propsA[k] !== propsB[k])
|
||||
return keys.filter((k)=> !(k in propsA) || propsA[k] !== undefined && propsA[k] !== propsB[k])
|
||||
}
|
||||
|
||||
|
||||
function dirtyItems(a, b) {
|
||||
const result = {}
|
||||
for (const k of dirtyKeys(this.tvProps, props))
|
||||
result[k] = props[k]
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -278,113 +291,173 @@ class ShapeTVCallbacks {
|
||||
}
|
||||
|
||||
onCreate(shapeId, _tvShape, points, props) {
|
||||
if( this.shape.id )
|
||||
throw Error(`Created a shape ${shapeId} where one already existed ${this.shape.id}`)
|
||||
this.shape.id = shapeId
|
||||
if( this.shape.lock ) return
|
||||
this.creating = true
|
||||
invokeCallback(this.shape, 'onCreate', points, props)
|
||||
}
|
||||
|
||||
onPoints(shapeId, _tvShape, points) {
|
||||
this.shape.points = points
|
||||
if (this.shape.debug) console.log('tvcb onPoints', points)
|
||||
this.shape.tvPoints = points
|
||||
this.shape.onPoints(points)
|
||||
this.shape.onDirtyModel(this.shape.pointsToModel)
|
||||
}
|
||||
|
||||
onProps(shapeId, _tvShape, props) {
|
||||
// console.log('onProps', props)
|
||||
if (this.creating) {
|
||||
// if (this.shape.debug) console.log('tvOnProps', props)
|
||||
if (this.creating) { // todo still useful?
|
||||
this.creating = false
|
||||
return
|
||||
}
|
||||
this.shape.props = props
|
||||
this.shape.tvProps = props
|
||||
this.shape.onProps(props)
|
||||
this.shape.onDirtyModel(this.shape.propsToModel)
|
||||
}
|
||||
|
||||
onDraw() {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onDraw')
|
||||
}
|
||||
|
||||
onRedraw() {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onRedraw')
|
||||
}
|
||||
|
||||
onUndraw() {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onUndraw')
|
||||
}
|
||||
|
||||
onAddPoint() {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onAddPoint')
|
||||
}
|
||||
|
||||
onMove(_shapeId, _tvShape, points) {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onMove',points)
|
||||
}
|
||||
|
||||
onDrag(_shapeId, _tvShape, points) {
|
||||
if( this.shape.lock ) return
|
||||
if (this.shape.debug) console.log('onDrag')
|
||||
invokeCallback(this.shape, 'onDrag', points)
|
||||
}
|
||||
|
||||
onHide(_shapeId, _tvShape, props) {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onHide',props)
|
||||
}
|
||||
|
||||
onShow(_shapeId, _tvShape, props) {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onShow',props)
|
||||
}
|
||||
|
||||
onClick(_shapeId, _tvShape) {
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onClick')
|
||||
}
|
||||
|
||||
onDelete(shapeId) {
|
||||
this.shape.id = null
|
||||
if( this.shape.lock ) return
|
||||
invokeCallback(this.shape, 'onDelete', shapeId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class HLine extends Shape {
|
||||
export class Line extends Shape {
|
||||
onDrag(points) {
|
||||
const s = this.tvShape();
|
||||
if (this.debug) {
|
||||
console.log('shape', s)
|
||||
console.log('currentMovingPoint', s._source.currentMovingPoint())
|
||||
console.log('startMovingPoint', s._source.startMovingPoint())
|
||||
console.log('isBeingEdited', s._source.isBeingEdited())
|
||||
console.log('state', s._source.state())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class HLine extends Line {
|
||||
constructor(model, onModel=null, onDelete=null, props=null) {
|
||||
super(ShapeType.HLine, model, onModel, onDelete, props)
|
||||
}
|
||||
|
||||
pointsFromModel() {
|
||||
if (this.model.price === null) return null
|
||||
const time = this.points !== null && this.points.length > 0 ? this.points[0].time : 0
|
||||
return [{time:time, price:this.model.price}]
|
||||
setModel(model) {
|
||||
if (model.price !== undefined && model.price !== this.model.price) {
|
||||
this.model.price = model.price
|
||||
this.setPoints([{time:0,price:this.model.price}])
|
||||
}
|
||||
}
|
||||
|
||||
pointsToModel() {
|
||||
this.model.price = this.points[0].price
|
||||
onPoints(points) {
|
||||
super.onPoints(points);
|
||||
}
|
||||
|
||||
propsFromModel() {
|
||||
return this.model.color ? {linecolor: this.model.color} : null
|
||||
pointsFromModel(model) {
|
||||
if (model.price === null || model.price===undefined) return null
|
||||
// take any time available, or 0
|
||||
const time =
|
||||
this.ourPoints !== null && this.ourPoints.length > 0 ? this.ourPoints[0].time :
|
||||
this.tvPoints !== null && this.tvPoints.length > 0 ? this.tvPoints[0].time : 0
|
||||
return [{time:time, price:model.price}]
|
||||
}
|
||||
|
||||
propsToModel() {this.model.color=this.props.linecolor}
|
||||
|
||||
onDrag(points) {
|
||||
const s = this.tvShape();
|
||||
console.log('shape', s)
|
||||
console.log('currentMovingPoint', s._source.currentMovingPoint())
|
||||
console.log('startMovingPoint', s._source.startMovingPoint())
|
||||
console.log('isBeingEdited', s._source.isBeingEdited())
|
||||
console.log('state', s._source.state())
|
||||
pointsToModel(points) {
|
||||
return {price: this.ourPoints[0].price}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function timeAdjustmentTooSmall(orig, newValue) {
|
||||
// TradingView adjusts our lines to be at the start of the intervals, so
|
||||
// we ignore deltas smaller than one interval prior
|
||||
return newValue === undefined ||
|
||||
orig !== null && orig !== undefined && newValue !== null &&
|
||||
newValue < orig && orig - newValue < useChartOrderStore().intervalSecs
|
||||
}
|
||||
|
||||
|
||||
function ohlcStart(time) {
|
||||
const period = useChartOrderStore().intervalSecs
|
||||
return Math.floor(time/period) * period
|
||||
}
|
||||
|
||||
|
||||
export class VLine extends Line {
|
||||
constructor(model, onModel=null, onDelete=null, props=null) {
|
||||
super(ShapeType.VLine, onModel, onDelete, props)
|
||||
|
||||
// Model
|
||||
this.model.time = null
|
||||
|
||||
this.setModel(model) // call setModel at the end
|
||||
}
|
||||
|
||||
onPoints(points) {
|
||||
if (this.debug) console.log('vline onPoints', this.ourPoints, points)
|
||||
super.onPoints(points);
|
||||
const orig = this.ourPoints && this.ourPoints.length ? this.ourPoints[0].time : null
|
||||
if (!timeAdjustmentTooSmall(orig, points[0].time)) {
|
||||
if (this.debug) console.log('updateModel', points[0].time)
|
||||
this.updateModel({time: points[0].time})
|
||||
}
|
||||
}
|
||||
|
||||
setModel(model) {
|
||||
if (this.debug) console.log('vline setModel', this.model.time, model )
|
||||
super.setModel(model)
|
||||
if (model.time !== undefined && model.time !== this.model.time) {
|
||||
this.model.time = model.time
|
||||
const time = ohlcStart(model.time);
|
||||
if (this.debug) console.log('vline setPoints', this.id, time)
|
||||
this.setPoints([{time, price:1}])
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.model.time = null
|
||||
super.delete()
|
||||
}
|
||||
|
||||
dirtyPoints(pointsA, pointsB) {
|
||||
const a = pointsA ? pointsA[0].time : null
|
||||
const b = pointsB ? pointsB[0].time : null
|
||||
const result = !timeAdjustmentTooSmall(a, b)
|
||||
if (this.debug) console.log('vline dirty points?', a, b, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user